<?php declare(strict_types=1);

namespace Phan;

use Phan\Library\Paths;
use function array_key_exists;
use function gettype;
use function is_array;
use function is_bool;
use function is_float;
use function is_int;
use function is_null;
use function is_string;
use const PHP_EOL;
use const PHP_VERSION;
use const STDERR;

/**
 * Program configuration.
 *
 * Many of the settings in this class can be overridden in .phan/config.php.
 *
 * Some configuration can be overridden on the command line.
 * See `./phan -h` for command line usage, or take a
 * look at \Phan\CLI.php for more details on CLI usage.
 *
 * For efficiency, all of these methods are static methods.
 * Configuration is fetched frequently, and static methods were much faster than magic __get().
 * @phan-file-suppress PhanPluginDescriptionlessCommentOnPublicMethod
 */
class Config
{
    /**
     * The version of the AST (defined in php-ast) that we're using.
     * Older versions are likely to have edge cases we no longer support,
     * and version 50 got rid of Decl.
     *
     * TODO: Switch to version 70 in the next major Phan release.
     */
    const AST_VERSION = 50;

    /**
     * The version of the Phan plugin system.
     * Plugin files that wish to be backwards compatible may check this and
     * return different classes based on its existence and
     * the results of version_compare.
     * PluginV2 will correspond to 2.x.y, PluginV3 will correspond to 3.x.y, etc.
     * New features increment minor versions, and bug fixes increment patch versions.
     * @suppress PhanUnreferencedPublicClassConstant
     */
    const PHAN_PLUGIN_VERSION = '2.9.0';

    /**
     * @var string|null
     * The root directory of the project. This is used to
     * store canonical path names and find project resources
     */
    private static $project_root_directory = null;

    /**
     * Configuration options
     */
    private static $configuration = self::DEFAULT_CONFIGURATION;

    // The most commonly accessed configs:
    /** @var bool mirror of configuration with the same name */
    private static $null_casts_as_any_type = false;

    /** @var bool replicates configuration with the same name */
    private static $null_casts_as_array = false;

    /** @var bool replicates configuration with the same name */
    private static $array_casts_as_null = false;

    /** @var bool replicates configuration with the same name */
    private static $strict_method_checking = false;

    /** @var bool replicates configuration with the same name */
    private static $strict_param_checking = false;

    /** @var bool replicates configuration with the same name */
    private static $strict_property_checking = false;

    /** @var bool replicates configuration with the same name */
    private static $strict_return_checking = false;

    /** @var bool replicates configuration with the same name */
    private static $track_references = false;

    /** @var bool replicates configuration with the same name */
    private static $backward_compatibility_checks = false;

    /** @var bool replicates configuration with the same name */
    private static $quick_mode = false;
    // End of the most commonly accessed configs.

    /** @var int the 5-digit PHP version id which is closest to matching the PHP_VERSION_ID for the 'target_php_version' string */
    private static $closest_target_php_version_id;

    /**
     * This constant contains the default values for Phan's configuration settings.
     *
     * Both your project's `.phan/config.php` file and the CLI flags used to invoke Phan
     * will override these defaults.
     *
     * NOTE: The line comments for individual configuration settings are written in markdown.
     * They are extracted by `\Phan\Config\Initializer` and used in the following places:
     *
     * 1. The configuration automatically generated by `phan --init`.
     * 2. The GitHub Wiki documentation generated by `internal/update_wiki_config_types.php`.
     */
    const DEFAULT_CONFIGURATION = [
        // Supported values: `'5.6'`, `'7.0'`, `'7.1'`, `'7.2'`, `'7.3'`, `'7.4'`, `null`.
        // If this is set to `null`,
        // then Phan assumes the PHP version which is closest to the minor version
        // of the php executable used to execute Phan.
        //
        // Note that the **only** effect of choosing `'5.6'` is to infer that functions removed in php 7.0 exist.
        // (See `backward_compatibility_checks` for additional options)
        'target_php_version' => null,

        // Default: true. If this is set to true,
        // and `target_php_version` is newer than the version used to run Phan,
        // Phan will act as though functions added in newer PHP versions exist.
        //
        // NOTE: Currently, this only affects `Closure::fromCallable()`
        'pretend_newer_core_methods_exist' => true,

        // Make the tolerant-php-parser polyfill generate doc comments
        // for all types of elements, even if php-ast wouldn't (for an older PHP version)
        'polyfill_parse_all_element_doc_comments' => true,

        // A list of individual files to include in analysis
        // with a path relative to the root directory of the
        // project.
        'file_list' => [],

        // A list of directories that should be parsed for class and
        // method information. After excluding the directories
        // defined in `exclude_analysis_directory_list`, the remaining
        // files will be statically analyzed for errors.
        //
        // Thus, both first-party and third-party code being used by
        // your application should be included in this list.
        'directory_list' => [],

        // For internal use by Phan to quickly check for membership in directory_list.
        '__directory_regex' => null,

        // Whether to enable debugging output to stderr
        'debug_output' => false,

        // List of case-insensitive file extensions supported by Phan.
        // (e.g. `['php', 'html', 'htm']`)
        'analyzed_file_extensions' => ['php'],

        // A regular expression to match files to be excluded
        // from parsing and analysis and will not be read at all.
        //
        // This is useful for excluding groups of test or example
        // directories/files, unanalyzable files, or files that
        // can't be removed for whatever reason.
        // (e.g. `'@Test\.php$@'`, or `'@vendor/.*/(tests|Tests)/@'`)
        'exclude_file_regex' => '',

        // A file list that defines files that will be excluded
        // from parsing and analysis and will not be read at all.
        //
        // This is useful for excluding hopelessly unanalyzable
        // files that can't be removed for whatever reason.
        'exclude_file_list' => [],

        // Enable this to enable checks of require/include statements referring to valid paths.
        'enable_include_path_checks' => false,

        // A list of [include paths](https://secure.php.net/manual/en/ini.core.php#ini.include-path) to check when checking if `require_once`, `include`, etc. are pointing to valid files.
        //
        // To refer to the directory of the file being analyzed, use `'.'`
        // To refer to the project root directory, use \Phan\Config::getProjectRootDirectory()
        //
        // (E.g. `['.', \Phan\Config::getProjectRootDirectory() . '/src/folder-added-to-include_path']`)
        //
        // This is ignored if `enable_include_path_checks` is not `true`.
        'include_paths' => ['.'],

        // Enable this to warn about the use of relative paths in `require_once`, `include`, etc.
        // Relative paths are harder to reason about, and opcache may have issues with relative paths in edge cases.
        //
        // This is ignored if `enable_include_path_checks` is not `true`.
        'warn_about_relative_include_statement' => false,

        // A directory list that defines files that will be excluded
        // from static analysis, but whose class and method
        // information should be included.
        //
        // Generally, you'll want to include the directories for
        // third-party code (such as "vendor/") in this list.
        //
        // n.b.: If you'd like to parse but not analyze 3rd
        //       party code, directories containing that code
        //       should be added to the `directory_list` as well as
        //       to `exclude_analysis_directory_list`.
        'exclude_analysis_directory_list' => [],

        // This is set internally by Phan based on exclude_analysis_directory_list
        '__exclude_analysis_regex' => null,

        // A file list that defines files that will be included
        // in static analysis, to the exclusion of others.
        'include_analysis_file_list' => [],

        // Backwards Compatibility Checking. This is slow
        // and expensive, but you should consider running
        // it before upgrading your version of PHP to a
        // new version that has backward compatibility
        // breaks.
        //
        // If you are migrating from PHP 5 to PHP 7,
        // you should also look into using
        // [php7cc (no longer maintained)](https://github.com/sstalle/php7cc)
        // and [php7mar](https://github.com/Alexia/php7mar),
        // which have different backwards compatibility checks.
        'backward_compatibility_checks' => true,

        // A set of fully qualified class-names for which
        // a call to `parent::__construct()` is required.
        'parent_constructor_required' => [],

        // If true, this runs a quick version of checks that takes less
        // time at the cost of not running as thorough
        // of an analysis. You should consider setting this
        // to true only when you wish you had more **undiagnosed** issues
        // to fix in your code base.
        //
        // In quick-mode the scanner doesn't rescan a function
        // or a method's code block every time a call is seen.
        // This means that the problem here won't be detected:
        //
        // ```php
        // <?php
        // function test($arg):int {
        //     return $arg;
        // }
        // test("abc");
        // ```
        //
        // This would normally generate:
        //
        // ```
        // test.php:3 PhanTypeMismatchReturn Returning type string but test() is declared to return int
        // ```
        //
        // The initial scan of the function's code block has no
        // type information for `$arg`. It isn't until we see
        // the call and rescan `test()`'s code block that we can
        // detect that it is actually returning the passed in
        // `string` instead of an `int` as declared.
        'quick_mode' => false,

        // The maximum recursion depth that can be reached when analyzing the code.
        // This setting only takes effect when quick_mode is disabled.
        // A higher limit will make the analysis more accurate, but could possibly
        // make it harder to track the code bit where a detected issue originates.
        // As long as this is kept relatively low, performance is usually not affected
        // by changing this setting.
        'maximum_recursion_depth' => 2,

        // If enabled, check all methods that override a
        // parent method to make sure its signature is
        // compatible with the parent's.
        //
        // This check can add quite a bit of time to the analysis.
        //
        // This will also check if final methods are overridden, etc.
        'analyze_signature_compatibility' => true,

        // Set this to true to allow contravariance in real parameter types of method overrides
        // (Users may enable this if analyzing projects that support only php 7.2+)
        //
        // See [this note about PHP 7.2's new features](https://secure.php.net/manual/en/migration72.new-features.php#migration72.new-features.param-type-widening).
        // This is false by default. (By default, Phan will warn if real parameter types are omitted in an override)
        //
        // If this is overridden to be null, this will be inferred from `target_php_version`.
        'allow_method_param_type_widening' => false,

        // Set this to true to make Phan guess that undocumented parameter types
        // (for optional parameters) have the same type as default values
        // (Instead of combining that type with `mixed`).
        //
        // E.g. `function my_method($x = 'val')` would make Phan infer that `$x` had a type of `string`, not `string|mixed`.
        // Phan will not assume it knows specific types if the default value is `false` or `null`.
        'guess_unknown_parameter_type_using_default' => false,

        // If enabled, inherit any missing phpdoc for types from
        // the parent method if none is provided.
        //
        // NOTE: This step will only be performed if `analyze_signature_compatibility` is also enabled.
        'inherit_phpdoc_types' => true,

        // The minimum severity level to report on. This can be
        // set to `Issue::SEVERITY_LOW`, `Issue::SEVERITY_NORMAL` or
        // `Issue::SEVERITY_CRITICAL`. Setting it to only
        // critical issues is a good place to start on a big
        // sloppy mature code base.
        'minimum_severity' => Issue::SEVERITY_LOW,

        // If enabled, missing properties will be created when
        // they are first seen. If false, we'll report an
        // error message if there is an attempt to write
        // to a class property that wasn't explicitly
        // defined.
        'allow_missing_properties' => false,

        // If enabled, allow null to be cast as any array-like type.
        //
        // This is an incremental step in migrating away from `null_casts_as_any_type`.
        // If `null_casts_as_any_type` is true, this has no effect.
        'null_casts_as_array' => false,

        // If enabled, allow any array-like type to be cast to null.
        // This is an incremental step in migrating away from `null_casts_as_any_type`.
        // If `null_casts_as_any_type` is true, this has no effect.
        'array_casts_as_null' => false,

        // If enabled, null can be cast to any type and any
        // type can be cast to null. Setting this to true
        // will cut down on false positives.
        'null_casts_as_any_type' => false,

        // If enabled, Phan will warn if **any** type in a method invocation's object
        // is definitely not an object,
        // or if **any** type in an invoked expression is not a callable.
        // Setting this to true will introduce numerous false positives
        // (and reveal some bugs).
        'strict_method_checking' => false,

        // If enabled, Phan will warn if **any** type in the argument's union type
        // cannot be cast to a type in the parameter's expected union type.
        // Setting this to true will introduce numerous false positives
        // (and reveal some bugs).
        'strict_param_checking' => false,

        // If enabled, Phan will warn if **any** type in a property assignment's union type
        // cannot be cast to a type in the property's declared union type.
        // Setting this to true will introduce numerous false positives
        // (and reveal some bugs).
        'strict_property_checking' => false,

        // If enabled, Phan will warn if **any** type in a returned value's union type
        // cannot be cast to the declared return type.
        // Setting this to true will introduce numerous false positives
        // (and reveal some bugs).
        'strict_return_checking' => false,

        // If enabled, scalars (int, float, bool, string, null)
        // are treated as if they can cast to each other.
        // This does not affect checks of array keys. See `scalar_array_key_cast`.
        'scalar_implicit_cast' => false,

        // If enabled, any scalar array keys (int, string)
        // are treated as if they can cast to each other.
        // E.g. `array<int,stdClass>` can cast to `array<string,stdClass>` and vice versa.
        // Normally, a scalar type such as int could only cast to/from int and mixed.
        'scalar_array_key_cast' => false,

        // If this has entries, scalars (int, float, bool, string, null)
        // are allowed to perform the casts listed.
        //
        // E.g. `['int' => ['float', 'string'], 'float' => ['int'], 'string' => ['int'], 'null' => ['string']]`
        // allows casting null to a string, but not vice versa.
        // (subset of `scalar_implicit_cast`)
        'scalar_implicit_partial' => [],

        // If true, seemingly undeclared variables in the global
        // scope will be ignored.
        //
        // This is useful for projects with complicated cross-file
        // globals that you have no hope of fixing.
        'ignore_undeclared_variables_in_global_scope' => false,

        // If true, check to make sure the return type declared
        // in the doc-block (if any) matches the return type
        // declared in the method signature.
        'check_docblock_signature_return_type_match' => true,

        // If true, check to make sure the param types declared
        // in the doc-block (if any) matches the param types
        // declared in the method signature.
        'check_docblock_signature_param_type_match' => true,

        // If true, make narrowed types from phpdoc params override
        // the real types from the signature, when real types exist.
        // (E.g. allows specifying desired lists of subclasses,
        //  or to indicate a preference for non-nullable types over nullable types)
        //
        // Affects analysis of the body of the method and the param types passed in by callers.
        //
        // (*Requires `check_docblock_signature_param_type_match` to be true*)
        'prefer_narrowed_phpdoc_param_type' => true,

        // (*Requires `check_docblock_signature_return_type_match` to be true*)
        //
        // If true, make narrowed types from phpdoc returns override
        // the real types from the signature, when real types exist.
        //
        // (E.g. allows specifying desired lists of subclasses,
        // or to indicate a preference for non-nullable types over nullable types)
        //
        // This setting affects the analysis of return statements in the body of the method and the return types passed in by callers.
        'prefer_narrowed_phpdoc_return_type' => true,

        // Set to true in order to attempt to detect dead
        // (unreferenced) code. Keep in mind that the
        // results will only be a guess given that classes,
        // properties, constants and methods can be referenced
        // as variables (like `$class->$property` or
        // `$class->$method()`) in ways that we're unable
        // to make sense of.
        'dead_code_detection' => false,

        // Set to true in order to attempt to detect unused variables.
        // `dead_code_detection` will also enable unused variable detection.
        //
        // This has a few known false positives, e.g. for loops or branches.
        'unused_variable_detection' => false,

        // Set to true in order to attempt to detect variables that could be replaced with constants or literals.
        // (i.e. they are declared once (as a constant expression) and never modified)
        // This is almost entirely false positives for most coding styles.
        //
        // This is intended to be used to check for bugs where a variable such as a boolean was declared but is no longer (or was never) modified.
        'constant_variable_detection' => false,

        // Set to true in order to emit issues such as `PhanUnusedPublicMethodParameter` instead of `PhanUnusedPublicNoOverrideMethodParameter`
        // (i.e. assume any non-final non-private method can have overrides).
        // This is useful in situations when parsing only a subset of the available files.
        'unused_variable_detection_assume_override_exists' => false,

        // Set this to true in order to aggressively assume class elements aren't overridden when analyzing uses of classes.
        // This is useful for standalone applications which have all code analyzed by Phan.
        //
        // Currently, this just affects inferring that methods without return statements have type `void`
        'assume_no_external_class_overrides' => false,

        // Set to true in order to force tracking references to elements
        // (functions/methods/consts/protected).
        //
        // `dead_code_detection` is another option which also causes references
        // to be tracked.
        'force_tracking_references' => false,

        // If true, the dead code detection rig will
        // prefer false negatives (not report dead code) to
        // false positives (report dead code that is not
        // actually dead).
        //
        // In other words, the graph of references will have
        // too many edges rather than too few edges when guesses
        // have to be made about what references what.
        'dead_code_detection_prefer_false_negative' => true,

        // If true, then before analysis, try to simplify AST into a form
        // which improves Phan's type inference in edge cases.
        //
        // This may conflict with `dead_code_detection`.
        // When this is true, this slows down analysis slightly.
        //
        // E.g. rewrites `if ($a = value() && $a > 0) {...}`
        // into `$a = value(); if ($a) { if ($a > 0) {...}}`
        'simplify_ast' => true,

        // Enable this to warn about harmless redundant use for classes and namespaces such as `use Foo\bar` in namespace Foo.
        //
        // Note: This does not affect warnings about redundant uses in the global namespace.
        'warn_about_redundant_use_namespaced_class' => false,

        // If true, Phan will read `class_alias()` calls in the global scope, then
        //
        // 1. create aliases from the *parsed* files if no class definition was found, and
        // 2. emit issues in the global scope if the source or target class is invalid.
        //    (If there are multiple possible valid original classes for an aliased class name,
        //    the one which will be created is unspecified.)
        //
        // NOTE: THIS IS EXPERIMENTAL, and the implementation may change.
        'enable_class_alias_support' => false,

        // If disabled, Phan will not read docblock type
        // annotation comments for `@property`.
        //
        // - When enabled, in addition to inferring existence of magic properties,
        //   Phan will also warn when writing to `@property-read` and reading from `@property-read`.
        // Phan will warn when writing to read-only properties and reading from write-only properties.
        //
        // Note: `read_type_annotations` must also be enabled.
        'read_magic_property_annotations' => true,

        // If disabled, Phan will not read docblock type
        // annotation comments for `@method`.
        //
        // Note: `read_type_annotations` must also be enabled.
        'read_magic_method_annotations' => true,

        // If disabled, Phan will not read docblock type
        // annotation comments (such as for `@return`, `@param`,
        // `@var`, `@suppress`, `@deprecated`) and only rely on
        // types expressed in code.
        'read_type_annotations' => true,

        // If enabled, Phan will cache ASTs generated by the polyfill/fallback to disk
        // (except when running in the background as a language server/daemon)
        //
        // ASTs generated by the native AST library (php-ast) are never cached,
        // because php-ast is faster than loading and unserializing data from the cache.
        //
        // Disabling this is faster when the cache won't be reused,
        // e.g. if this would be run in a docker image without mounting the cache as a volume.
        //
        // The cache can be found at `sys_get_tmp_dir() . "/phan-$USERNAME"`.
        'cache_polyfill_asts' => true,

        // If enabled, warn about throw statement where the exception types
        // are not documented in the PHPDoc of functions, methods, and closures.
        'warn_about_undocumented_throw_statements' => false,

        // If enabled (and `warn_about_undocumented_throw_statements` is enabled),
        // Phan will warn about function/closure/method invocations that have `@throws`
        // that aren't caught or documented in the invoking method.

        'warn_about_undocumented_exceptions_thrown_by_invoked_functions' => false,

        // Phan will not warn about lack of documentation of `@throws` for any of the configured classes or their subclasses.
        // This only matters when `warn_about_undocumented_throw_statements` is true.
        // The default is the empty array (Don't suppress any warnings)
        //
        // (E.g. `['RuntimeException', 'AssertionError', 'TypeError']`)
        'exception_classes_with_optional_throws_phpdoc' => [ ],

        // This setting maps case-insensitive strings to union types.
        //
        // This is useful if a project uses phpdoc that differs from the phpdoc2 standard.
        //
        // If the corresponding value is the empty string,
        // then Phan will ignore that union type (E.g. can ignore 'the' in `@return the value`)
        //
        // If the corresponding value is not empty,
        // then Phan will act as though it saw the corresponding UnionTypes(s)
        // when the keys show up in a UnionType of `@param`, `@return`, `@var`, `@property`, etc.
        //
        // This matches the **entire string**, not parts of the string.
        // (E.g. `@return the|null` will still look for a class with the name `the`, but `@return the` will be ignored with the below setting)
        //
        // (These are not aliases, this setting is ignored outside of doc comments).
        // (Phan does not check if classes with these names exist)
        //
        // Example setting: `['unknown' => '', 'number' => 'int|float', 'char' => 'string', 'long' => 'int', 'the' => '']`
        'phpdoc_type_mapping' => [ ],

        // Set to true in order to ignore issue suppression.
        // This is useful for testing the state of your code, but
        // unlikely to be useful outside of that.
        'disable_suppression' => false,

        // Set to true in order to ignore line-based issue suppressions.
        // Disabling both line and file-based suppressions is mildly faster.
        'disable_line_based_suppression' => false,

        // Set to true in order to ignore file-based issue suppressions.
        'disable_file_based_suppression' => false,

        // If set to true, we'll dump the AST instead of
        // analyzing files
        'dump_ast' => false,

        // If set to a string, we'll dump the fully qualified lowercase
        // function and method signatures instead of analyzing files.
        'dump_signatures_file' => null,

        // If set to true, we'll dump the list of files to parse
        // to stdout instead of parsing and analyzing files.
        'dump_parsed_file_list' => false,

        // Include a progress bar in the output.
        'progress_bar' => false,

        // If this much time (in seconds) has passed since the last update,
        // then update the progress bar.
        'progress_bar_sample_interval' => 0.1,

        // The number of processes to fork off during the analysis
        // phase.
        'processes' => 1,

        // Set to true to emit profiling data on how long various
        // parts of Phan took to run. You likely don't care to do
        // this.
        'profiler_enabled' => false,

        // Phan will give up on suggesting a different name in issue messages
        // if the number of candidates (for a given suggestion category) is greater than `suggestion_check_limit`.
        //
        // Set this to `0` to disable most suggestions for similar names, and only suggest identical names in other namespaces.
        // Set this to `PHP_INT_MAX` (or other large value) to always suggest similar names and identical names in other namespaces.
        //
        // Phan will be a bit slower when this config setting is large.
        // A lower value such as 50 works for suggesting misspelled classes/constants in namespaces,
        // but won't give you suggestions for globally namespaced functions.
        'suggestion_check_limit' => 1000,

        // Set this to true to disable suggestions for what to use instead of undeclared variables/classes/etc.
        'disable_suggestions' => false,

        // Add any issue types (such as `'PhanUndeclaredMethod'`)
        // to this black-list to inhibit them from being reported.
        'suppress_issue_types' => [
            // 'PhanUndeclaredMethod',
        ],

        // If empty, no filter against issues types will be applied.
        // If this white-list is non-empty, only issues within the list
        // will be emitted by Phan.
        //
        // See https://github.com/phan/phan/wiki/Issue-Types-Caught-by-Phan
        // for the full list of issues that Phan detects.
        //
        // Phan is capable of detecting hundreds of types of issues.
        // Projects should almost always use `suppress_issue_types` instead.
        'whitelist_issue_types' => [
            // 'PhanUndeclaredClass',
        ],

        // A custom list of additional superglobals and their types. **Only needed by projects using runkit/runkit7.**
        //
        // (Corresponding keys are declared in `runkit.superglobal` ini directive)
        //
        // `globals_type_map` should be set for setting the types of these superglobals.
        // E.g `['_FOO']`;
        'runkit_superglobals' => [],

        // Override to hardcode existence and types of (non-builtin) globals in the global scope.
        // Class names should be prefixed with `\`.
        //
        // (E.g. `['_FOO' => '\FooClass', 'page' => '\PageClass', 'userId' => 'int']`)
        'globals_type_map' => [],

        // Enable this to emit issue messages with markdown formatting.
        'markdown_issue_messages' => false,

        // Emit colorized issue messages.
        // NOTE: it is strongly recommended to enable this via the `--color` CLI flag instead,
        // since this is incompatible with most output formatters.
        'color_issue_messages' => false,

        // Allow overriding color scheme in `.phan/config.php` for printing issues, for individual types.
        //
        // See the keys of `Phan\Output\Colorizing::STYLES` for valid color names,
        // and the keys of `Phan\Output\Colorizing::DEFAULT_COLOR_FOR_TEMPLATE` for valid color names.
        //
        // E.g. to change the color for the file (of an issue instance) to red, set this to `['FILE' => 'red']`
        //
        // E.g. to use the terminal's default color for the line (of an issue instance), set this to `['LINE' => 'none']`
        'color_scheme' => [],

        // Enable or disable support for generic templated
        // class types.
        'generic_types_enabled' => true,

        // Assign files to be analyzed on random processes
        // in random order. You very likely don't want to
        // set this to true. This is meant for debugging
        // and fuzz testing purposes only.
        'randomize_file_order' => false,

        // Setting this to true makes the process assignment for file analysis
        // as predictable as possible, using consistent hashing.
        //
        // Even if files are added or removed, or process counts change,
        // relatively few files will move to a different group.
        // (use when the number of files is much larger than the process count)
        //
        // NOTE: If you rely on Phan parsing files/directories in the order
        // that they were provided in this config, don't use this.
        // See [this note in Phan's wiki](https://github.com/phan/phan/wiki/Different-Issue-Sets-On-Different-Numbers-of-CPUs).
        'consistent_hashing_file_order' => false,

        // Set by `--print-memory-usage-summary`. Prints a memory usage summary to stderr after analysis.
        'print_memory_usage_summary' => false,

        // By default, Phan will log error messages to stdout if PHP is using options that slow the analysis.
        // (e.g. PHP is compiled with `--enable-debug` or when using XDebug)
        'skip_slow_php_options_warning' => false,

        // By default, Phan will warn if the 'tokenizer' module isn't installed and enabled.
        'skip_missing_tokenizer_warning' => false,

        // This is the maximum frame length for crash reports
        'debug_max_frame_length' => 1000,

        // You can put paths to stubs of internal extensions in this config option.
        // If the corresponding extension is **not** loaded, then Phan will use the stubs instead.
        // Phan will continue using its detailed type annotations,
        // but load the constants, classes, functions, and classes (and their Reflection types)
        // from these stub files (doubling as valid php files).
        // Use a different extension from php to avoid accidentally loading these.
        // The `tools/make_stubs` script can be used to generate your own stubs (compatible with php 7.0+ right now)
        //
        // (e.g. `['xdebug' => '.phan/internal_stubs/xdebug.phan_php']`)
        'autoload_internal_extension_signatures' => [
        ],

        // Set this to false to emit `PhanUndeclaredFunction` issues for internal functions that Phan has signatures for,
        // but aren't available in the codebase, or the internal functions used to run Phan
        // (may lead to false positives if an extension isn't loaded)
        //
        // If this is true(default), then Phan will not warn.
        'ignore_undeclared_functions_with_known_signatures' => true,

        // If a file to be analyzed can't be parsed,
        // then use a slower PHP substitute for php-ast to try to parse the files.
        // This setting is ignored if a file is excluded from analysis.
        //
        // NOTE: it is strongly recommended to enable this via the `--use-fallback-parser` CLI flag instead,
        // since this may result in strange error messages for invalid files (e.g. if parsed but not analyzed).
        'use_fallback_parser' => false,

        // Use the polyfill parser based on tolerant-php-parser instead of the possibly missing native implementation
        //
        // NOTE: This makes parsing several times slower than the native implementation.
        //
        // NOTE: it is strongly recommended to enable this via the `--use-polyfill-parser` or `--force-polyfill-parser`
        // since this may result in strange error messages for invalid files (e.g. if parsed but not analyzed).
        'use_polyfill_parser' => false,

        // Path to a Unix socket for a daemon to listen to files to analyze. Use command line option instead.
        'daemonize_socket' => false,

        // If a daemon should listen to files to analyze over TCP.
        // This setting is mutually exclusive with `daemonize_socket`.
        'daemonize_tcp' => false,

        // TCP host for a daemon to listen to files to analyze.
        'daemonize_tcp_host' => '127.0.0.1',

        // TCP port (from 1024 to 65535) for a daemon to listen to files to analyze.
        'daemonize_tcp_port' => 4846,

        // If this is an array, it configures the way clients will communicate with the Phan language server.
        // Possibilities: Exactly one of
        //
        // 1. `['stdin' => true]`
        // 2. `['tcp-server' => string (address this server should listen on)]`
        // 3. `['tcp' => string (address client is listening on)]`
        'language_server_config' => false,

        // Valid values: false, true. Should only be set via CLI (`--language-server-analyze-only-on-save`)
        'language_server_analyze_only_on_save' => false,

        // Valid values: null, 'info'. Used when developing or debugging a language server client of Phan.
        'language_server_debug_level' => null,

        // Set this to true to emit all issues detected from the language server (e.g. invalid phpdoc in parsed files),
        // not just issues in files currently open in the editor/IDE.
        // This can be very verbose and has more false positives.
        'language_server_disable_output_filter' => false,

        // This should only be set by CLI (`--language-server-force-missing-pcntl` or `language-server-require-pcntl`), which will set this to true for debugging.
        // When true, this will manually back up the state of the PHP process and restore it.
        'language_server_use_pcntl_fallback' => false,

        // This should only be set via CLI (`--language-server-enable-go-to-definition`)
        // Affects "go to definition" and "go to type definition" of LSP.
        'language_server_enable_go_to_definition' => false,

        // This should only be set via CLI (`--language-server-enable-hover`)
        // Affects "hover" of LSP.
        'language_server_enable_hover' => false,

        // This should only be set via CLI (`--language-server-enable-completion`)
        // Affects "completion" of LSP.
        'language_server_enable_completion' => false,

        // Don't show the category name in issue messages.
        // This makes error messages slightly shorter.
        // Use `--language-server-hide-category` if you want to enable this.
        'language_server_hide_category_of_issues' => false,

        // Set this to false to disable the plugins that Phan uses to infer more accurate return types of `array_map`, `array_filter`, and many other functions.
        //
        // Phan is slightly faster when these are disabled.
        'enable_internal_return_type_plugins' => true,

        // Set this to true to enable the plugins that Phan uses to infer more accurate return types of `implode`, `json_decode`, and many other functions.
        //
        // Phan is slightly faster when these are disabled.
        'enable_extended_internal_return_type_plugins' => false,

        // If a literal string type exceeds this length,
        // then Phan converts it to a regular string type.
        // This setting cannot be less than 50.
        //
        // This setting can be overridden if users wish to store strings that are even longer than 50 bytes.
        'max_literal_string_type_length' => 200,

        'dump_matching_functions' => false,

        // A list of plugin files to execute.
        //
        // Plugins which are bundled with Phan can be added here by providing their name (e.g. `'AlwaysReturnPlugin'`)
        //
        // Documentation about available bundled plugins can be found [here](https://github.com/phan/phan/tree/master/.phan/plugins).
        //
        // Alternately, you can pass in the full path to a PHP file with the plugin's implementation (e.g. `'vendor/phan/phan/.phan/plugins/AlwaysReturnPlugin.php'`)
        'plugins' => [
        ],

        // This can be used by third-party plugins that expect configuration.
        //
        // E.g. this is used by `InvokePHPNativeSyntaxCheckPlugin`
        'plugin_config' => [
        ],
    ];

    const COMPLETION_VSCODE = 'vscode';

    /**
     * Disallow the constructor.
     */
    private function __construct()
    {
    }

    /**
     * @return string
     * Get the root directory of the project that we're
     * scanning
     * @suppress PhanPossiblyFalseTypeReturn getcwd() can technically be false, but we should have checked earlier
     */
    public static function getProjectRootDirectory() : string
    {
        return self::$project_root_directory ?? \getcwd();
    }

    /**
     * @param string $project_root_directory
     * Set the root directory of the project that we're
     * scanning
     *
     * @return void
     */
    public static function setProjectRootDirectory(
        string $project_root_directory
    ) {
        self::$project_root_directory = $project_root_directory;
    }

    /**
     * @return void
     */
    public static function init()
    {
        static $did_init = false;
        if ($did_init) {
            return;
        }
        $did_init = true;
        self::initOnce();
    }

    private static function initOnce()
    {
        // Trigger magic setters
        foreach (self::$configuration as $name => $v) {
            self::setValue($name, $v);
        }
    }

    /**
     * @return array<string,mixed>
     * A map of configuration keys and their values
     */
    public static function toArray() : array
    {
        return self::$configuration;
    }

    // method naming is deliberate to make these getters easier to search.
    // phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps

    /**
     * Allow null to be cast as any type and for any
     * type to be cast to null.
     */
    public static function get_null_casts_as_any_type() : bool
    {
        return self::$null_casts_as_any_type;
    }

    /**
     * If enabled, Phan will warn if **any** type in a method's object expression
     * is definitely not an object,
     * or if **any** type in an invoked expression is not a callable.
     */
    public static function get_strict_method_checking() : bool
    {
        return self::$strict_method_checking;
    }

    /**
     * If enabled, Phan will warn if **any** type in the argument's type
     * cannot be cast to a type in the parameter's expected type.
     */
    public static function get_strict_param_checking() : bool
    {
        return self::$strict_param_checking;
    }

    /**
     * If enabled, Phan will warn if **any** type in a property assignment's type
     * cannot be cast to a type in the property's expected type.
     */
    public static function get_strict_property_checking() : bool
    {
        return self::$strict_property_checking;
    }

    /**
     * If enabled, Phan will warn if **any** type in the return statement's union type
     * cannot be cast to a type in the method's declared return type.
     */
    public static function get_strict_return_checking() : bool
    {
        return self::$strict_return_checking;
    }

    /** If enabled, allow null to cast to any array-like type. */
    public static function get_null_casts_as_array() : bool
    {
        return self::$null_casts_as_array;
    }

    /** If enabled, allow any array-like type to be cast to null. */
    public static function get_array_casts_as_null() : bool
    {
        return self::$array_casts_as_null;
    }

    /** If true, then Phan tracks references to elements */
    public static function get_track_references() : bool
    {
        return self::$track_references;
    }

    /** If true, then Phan enables backwards compatibility checking. */
    public static function get_backward_compatibility_checks() : bool
    {
        return self::$backward_compatibility_checks;
    }

    /**
     * If true, then Phan runs a quick version of checks that takes less
     * time at the cost of not running as thorough
     * of an analysis.
     */
    public static function get_quick_mode() : bool
    {
        return self::$quick_mode;
    }

    /** @return int the 5-digit PHP version id which is closest to matching the PHP_VERSION_ID for the 'target_php_version' string */
    public static function get_closest_target_php_version_id() : int
    {
        return self::$closest_target_php_version_id;
    }
    // phpcs:enable PSR1.Methods.CamelCapsMethodName.NotCamelCaps

    /**
     * @return mixed
     */
    public static function getValue(string $name)
    {
        return self::$configuration[$name];
    }

    /**
     * Resets the configuration to the initial state, prior to parsing config files and CLI arguments.
     * @return void
     * @internal - this should only be used in unit tests.
     */
    public static function reset()
    {
        self::$configuration = self::DEFAULT_CONFIGURATION;
        // Trigger magic behavior
        self::initOnce();
    }

    /**
     * @param string $name
     * @param mixed $value
     * @return void
     */
    public static function setValue(string $name, $value)
    {
        self::$configuration[$name] = $value;
        switch ($name) {
            case 'null_casts_as_any_type':
                self::$null_casts_as_any_type = $value;
                break;
            case 'null_casts_as_array':
                self::$null_casts_as_array = $value;
                break;
            case 'array_casts_as_null':
                self::$array_casts_as_null = $value;
                break;
            case 'strict_method_checking':
                self::$strict_method_checking = $value;
                break;
            case 'strict_param_checking':
                self::$strict_param_checking = $value;
                break;
            case 'strict_property_checking':
                self::$strict_property_checking = $value;
                break;
            case 'strict_return_checking':
                self::$strict_return_checking = $value;
                break;
            case 'dead_code_detection':
            case 'force_tracking_references':
                self::$track_references = self::getValue('dead_code_detection') || self::getValue('force_tracking_references');
                break;
            case 'backward_compatibility_checks':
                self::$backward_compatibility_checks = $value;
                break;
            case 'quick_mode':
                self::$quick_mode = $value;
                break;
            case 'allow_method_param_type_widening':
                self::$configuration['allow_method_param_type_widening_original'] = $value;
                if ($value === null) {
                    // If this setting is set to null, infer it based on the closest php version id.
                    self::$configuration[$name] = self::$closest_target_php_version_id >= 70200;
                }
                break;
            case 'target_php_version':
                if (is_float($value)) {
                    $value = \sprintf("%.1f", $value);
                }
                $value = (string) ($value ?: PHP_VERSION);
                if (\strtolower($value) === 'native') {
                    $value = PHP_VERSION;
                }

                self::$closest_target_php_version_id = self::computeClosestTargetPHPVersionId($value);
                if ((self::$configuration['allow_method_param_type_widening_original'] ?? null) === null) {
                    self::$configuration['allow_method_param_type_widening'] = self::$closest_target_php_version_id >= 70200;
                }
                break;
            case 'exclude_analysis_directory_list':
                self::$configuration['__exclude_analysis_regex'] = self::generateDirectoryListRegex($value);
                break;
            case 'directory_list':
                self::$configuration['__directory_regex'] = self::generateDirectoryListRegex($value);
                break;
        }
    }

    /**
     * @param string[] $value
     * @return ?string
     */
    private static function generateDirectoryListRegex(array $value)
    {
        if (!$value) {
            return null;
        }
        $parts = array_map(static function (string $path) : string {
            $path = \str_replace('\\', '/', $path);  // Normalize \\ to / in configs
            $path = \rtrim($path, '\//');  // remove trailing / from directory
            $path = \preg_replace('@^(\./)+@', '', $path);  // Remove any number of leading ./ sections
            return \preg_quote($path, '@');  // Quote this
        }, $value);

        return '@^(\./)*(' . implode('|', $parts) . ')([/\\\\]|$)@';
    }

    private static function computeClosestTargetPHPVersionId(string $version) : int
    {
        if (\version_compare($version, '6.0') < 0) {
            return 50600;
        } elseif (\version_compare($version, '7.1') < 0) {
            return 70000;
        } elseif (\version_compare($version, '7.2') < 0) {
            return 70100;
        } elseif (\version_compare($version, '7.3') < 0) {
            return 70200;
        } elseif (\version_compare($version, '7.4') < 0) {
            return 70300;
        } elseif (\version_compare($version, '8.0') < 0) {
            return 70400;
        } else {
            return 80000;
        }
    }

    /**
     * @return string
     * The relative path appended to the project root directory. (i.e. the absolute path)
     *
     * @suppress PhanUnreferencedPublicMethod
     * @see FileRef::getProjectRelativePathForPath() for converting to relative paths
     */
    public static function projectPath(string $relative_path) : string
    {
        return Paths::toAbsolutePath(self::getProjectRootDirectory(), $relative_path);
    }

    /**
     * @param mixed $value
     */
    private static function errSuffixGotType($value) : string
    {
        return ", but got type '" . gettype($value) . "'";
    }

    /**
     * @param array<string,mixed> $configuration
     * @return array<int,string> a list of 0 or more error messages for invalid config settings
     */
    public static function getConfigErrors(array $configuration) : array
    {
        /**
         * @param mixed $value
         * @return ?string
         */
        $is_scalar = static function ($value) {
            if (is_null($value) || \is_scalar($value)) {
                return null;
            }
            return 'Expected a scalar' . self::errSuffixGotType($value);
        };
        /**
         * @param mixed $value
         * @return ?string
         */
        $is_bool = static function ($value) {
            if (is_bool($value)) {
                return null;
            }
            return 'Expected a boolean' . self::errSuffixGotType($value);
        };
        /**
         * @param mixed $value
         * @return ?string
         */
        $is_string_or_null = static function ($value) {
            if (is_null($value) || is_string($value)) {
                return null;
            }
            return 'Expected a string' . self::errSuffixGotType($value);
        };
        /**
         * @param mixed $value
         * @return ?string
         */
        $is_string = static function ($value) {
            if (is_string($value)) {
                return null;
            }
            return 'Expected a string' . self::errSuffixGotType($value);
        };
        /**
         * @param mixed $value
         * @return ?string
         */
        $is_array = static function ($value) {
            if (is_array($value)) {
                return null;
            }
            return 'Expected an array' . self::errSuffixGotType($value);
        };
        /**
         * @param mixed $value
         * @return ?string
         */
        $is_int_strict = static function ($value) {
            if (is_int($value)) {
                return null;
            }
            return 'Expected an integer' . self::errSuffixGotType($value);
        };
        /**
         * @param mixed $value
         * @return ?string
         */
        $is_string_list = static function ($value) {
            if (!is_array($value)) {
                return 'Expected a list of strings' . self::errSuffixGotType($value);
            }
            foreach ($value as $i => $element) {
                if (!is_string($element)) {
                    return "Expected a list of strings: index $i is type '" . gettype($element) . "'";
                }
            }
            return null;
        };
        /**
         * @param mixed $value
         * @return ?string
         */
        $is_associative_string_array = static function ($value) {
            if (!is_array($value)) {
                return 'Expected an associative array mapping strings to strings'  . self::errSuffixGotType($value);
            }
            foreach ($value as $i => $element) {
                if (!is_string($element)) {
                    return "Expected an associative array mapping strings to strings: index $i is '" . gettype($element) . "'";
                }
            }
            return null;
        };
        $config_checks = [
            'allow_method_param_type_widening' => $is_bool,
            'allow_missing_properties' => $is_bool,
            'analyzed_file_extensions' => $is_string_list,
            'analyze_signature_compatibility' => $is_bool,
            'array_casts_as_null' => $is_bool,
            'autoload_internal_extension_signatures' => $is_associative_string_array,
            'backward_compatibility_checks' => $is_bool,
            'cache_polyfill_asts' => $is_bool,
            'check_docblock_signature_param_type_match' => $is_bool,
            'check_docblock_signature_return_type_match' => $is_bool,
            'color_issue_messages' => $is_bool,
            'color_scheme' => $is_associative_string_array,
            'consistent_hashing_file_order' => $is_bool,
            'daemonize_socket' => $is_scalar,
            'daemonize_tcp_host' => $is_string,
            'daemonize_tcp' => $is_bool,
            'daemonize_tcp_port' => $is_int_strict,
            'dead_code_detection' => $is_bool,
            'dead_code_detection_prefer_false_negative' => $is_bool,
            'directory_list' => $is_string_list,
            'disable_line_based_suppression' => $is_bool,
            'disable_suggestions' => $is_bool,
            'disable_suppression' => $is_bool,
            'dump_ast' => $is_bool,
            'dump_matching_functions' => $is_bool,
            'dump_parsed_file_list' => $is_bool,
            'dump_signatures_file' => $is_string_or_null,
            'enable_class_alias_support' => $is_bool,
            'enable_include_path_checks' => $is_bool,
            'enable_internal_return_type_plugins' => $is_bool,
            'exception_classes_with_optional_throws_phpdoc' => $is_string_list,
            'exclude_analysis_directory_list' => $is_string_list,
            'exclude_file_list' => $is_string_list,
            'exclude_file_regex' => $is_string_or_null,
            'file_list' => $is_string_list,
            'force_tracking_references' => $is_bool,
            'generic_types_enabled' => $is_bool,
            'globals_type_map' => $is_associative_string_array,
            'guess_unknown_parameter_type_using_default' => $is_bool,
            'ignore_undeclared_functions_with_known_signatures' => $is_bool,
            'ignore_undeclared_variables_in_global_scope' => $is_bool,
            'include_analysis_file_list' => $is_string_list,
            'include_paths' => $is_string_list,
            'inherit_phpdoc_types' => $is_bool,
            'language_server_analyze_only_on_save' => $is_bool,
            // 'language_server_config' => array|false,  // should not be set directly
            'language_server_debug_level' => $is_string_or_null,
            'language_server_disable_output_filter' => $is_bool,
            'language_server_enable_completion' => $is_scalar,
            'language_server_enable_go_to_definition' => $is_bool,
            'language_server_enable_hover' => $is_bool,
            'language_server_hide_category_of_issues' => $is_bool,
            'language_server_use_pcntl_fallback' => $is_bool,
            'markdown_issue_messages' => $is_bool,
            'max_literal_string_type_length' => $is_int_strict,
            'minimum_severity' => $is_int_strict,
            'null_casts_as_any_type' => $is_bool,
            'null_casts_as_array' => $is_bool,
            'parent_constructor_required' => $is_string_list,
            'phpdoc_type_mapping' => $is_associative_string_array,
            'plugin_config' => $is_array,
            'plugins' => $is_string_list,
            'polyfill_parse_all_element_doc_comments' => $is_bool,
            'prefer_narrowed_phpdoc_param_type' => $is_bool,
            'prefer_narrowed_phpdoc_return_type' => $is_bool,
            'pretend_newer_core_methods_exist' => $is_bool,
            'print_memory_usage_summary' => $is_bool,
            'processes' => $is_int_strict,
            'profiler_enabled' => $is_bool,
            'progress_bar' => $is_bool,
            'progress_bar_sample_interval' => $is_scalar,
            'quick_mode' => $is_bool,
            'randomize_file_order' => $is_bool,
            'read_magic_method_annotations' => $is_bool,
            'read_magic_property_annotations' => $is_bool,
            'read_type_annotations' => $is_bool,
            'runkit_superglobals' => $is_string_list,
            'scalar_array_key_cast' => $is_bool,
            'scalar_implicit_cast' => $is_bool,
            'scalar_implicit_partial' => $is_array,
            'simplify_ast' => $is_bool,
            'skip_missing_tokenizer_warning' => $is_bool,
            'skip_slow_php_options_warning' => $is_bool,
            'strict_method_checking' => $is_bool,
            'strict_param_checking' => $is_bool,
            'strict_property_checking' => $is_bool,
            'strict_return_checking' => $is_bool,
            'suggestion_check_limit' => $is_int_strict,
            'suppress_issue_types' => $is_string_list,
            'target_php_version' => $is_scalar,
            'unused_variable_detection' => $is_bool,
            'use_fallback_parser' => $is_bool,
            'use_polyfill_parser' => $is_bool,
            'warn_about_redundant_use_namespaced_class' => $is_bool,
            'warn_about_relative_include_statement' => $is_bool,
            'warn_about_undocumented_exceptions_thrown_by_invoked_functions' => $is_bool,
            'warn_about_undocumented_throw_statements' => $is_bool,
            'whitelist_issue_types' => $is_string_list,
        ];
        $result = [];
        foreach ($config_checks as $config_name => $check_closure) {
            if (!array_key_exists($config_name, $configuration)) {
                continue;
            }
            $value = $configuration[$config_name];
            $error = $check_closure($value);
            if ($error) {
                $result[] = "Invalid config value for '$config_name': $error";
            }
        }
        return $result;
    }

    /**
     * Prints errors to stderr if any config options are definitely invalid.
     * @return void
     */
    public static function warnIfInvalid()
    {
        $errors = self::getConfigErrors(self::$configuration);
        foreach ($errors as $error) {
            \fwrite(STDERR, $error . PHP_EOL);
        }
    }

    /**
     * Check if the issue fixing plugin (from --automatic-fix) is enabled.
     */
    public static function isIssueFixingPluginEnabled() : bool
    {
        return \in_array(__DIR__ . '/Plugin/Internal/IssueFixingPlugin.php', Config::getValue('plugins'));
    }
}

// Call init() to trigger the magic setters.
Config::init();
