/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use common::PointerAddress;
use fnv::FnvHashMap;
use graphql_ir::Condition;
use graphql_ir::FragmentDefinition;
use graphql_ir::InlineFragment;
use graphql_ir::LinkedField;
use graphql_ir::OperationDefinition;
use graphql_ir::Program;
use graphql_ir::ScalarField;
use graphql_ir::Selection;
use lazy_static::lazy_static;

lazy_static! {
    static ref LOG_AST_STATS: bool = std::env::var("RELAY_LOG_AST_STATS").is_ok();
}

pub fn print_stats(extra_info: &str, program: &Program) {
    if *LOG_AST_STATS {
        let mut visitor = IRStatsVisitor::default();
        let stats = visitor.visit_program(program);
        println!("{}", extra_info);
        stats.print_stats();
        println!();
    }
}

#[derive(Default)]
struct IRStatsVisitor {
    visited: FnvHashMap<PointerAddress, StatsCollection>,
}
/// Represents the stats of one category of the current node and it's descendant
/// total: Total number of nodes
/// duplicate: The number of nodes that are duplicate (doesn't include the original node)
/// total - duplicate = the number of nodes that actually consumes memory
#[derive(Default, Clone)]
struct Stats {
    total: usize,
    duplicate: usize,
}
#[derive(Default, Clone)]
struct StatsCollection {
    linked_field: Stats,
    scalar_field: Stats,
    inline_fragment: Stats,
    condition: Stats,
}

impl StatsCollection {
    fn print_stats(&self) {
        print!("Linked field: ");
        self.linked_field.print_stats();
        print!("Inline fragment: ");
        self.inline_fragment.print_stats();
        print!("Scalar field: ");
        self.scalar_field.print_stats();
        print!("Condition: ");
        self.condition.print_stats();
    }

    fn add(&mut self, other: StatsCollection) {
        self.linked_field.add(other.linked_field);
        self.scalar_field.add(other.scalar_field);
        self.inline_fragment.add(other.inline_fragment);
        self.condition.add(other.condition);
    }

    // If the Stats is read from a cache, that means it'a duplicated node:
    // all of it's descendants are also duplicates, thus the duplicate count
    // should be equalt to the total count.
    fn to_duplicate(&self) -> StatsCollection {
        StatsCollection {
            linked_field: self.linked_field.to_duplicate(),
            scalar_field: self.scalar_field.to_duplicate(),
            inline_fragment: self.inline_fragment.to_duplicate(),
            condition: self.condition.to_duplicate(),
        }
    }
}

impl Stats {
    fn print_stats(&self) {
        println!(
            "{} unique, {} total, {} duplicate.",
            self.total - self.duplicate,
            self.total,
            self.duplicate
        )
    }

    fn add(&mut self, other: Stats) {
        self.total += other.total;
        self.duplicate += other.duplicate;
    }

    fn to_duplicate(&self) -> Stats {
        Stats {
            total: self.total,
            duplicate: self.total,
        }
    }
}

impl IRStatsVisitor {
    fn visit_program(&mut self, program: &Program) -> StatsCollection {
        let mut result = StatsCollection::default();
        for operation in program.operations() {
            result.add(self.visit_operation(operation));
        }
        for fragment in program.fragments() {
            result.add(self.visit_fragment(fragment));
        }
        result
    }

    fn visit_operation(&mut self, operation: &OperationDefinition) -> StatsCollection {
        self.visit_selections(&operation.selections)
    }

    fn visit_fragment(&mut self, fragment: &FragmentDefinition) -> StatsCollection {
        self.visit_selections(&fragment.selections)
    }

    fn visit_selections(&mut self, selections: &[Selection]) -> StatsCollection {
        let mut result = StatsCollection::default();
        for selection in selections {
            result.add(self.visit_selection(selection))
        }
        result
    }

    fn visit_selection(&mut self, selection: &Selection) -> StatsCollection {
        match selection {
            Selection::FragmentSpread(_) => Default::default(),
            Selection::InlineFragment(selection) => {
                self.cached_visit_fn(selection.as_ref(), Self::visit_inline_fragment)
            }
            Selection::LinkedField(selection) => {
                self.cached_visit_fn(selection.as_ref(), Self::visit_linked_field)
            }
            Selection::ScalarField(selection) => {
                self.cached_visit_fn(selection.as_ref(), Self::visit_scalar_field)
            }
            Selection::Condition(selection) => {
                self.cached_visit_fn(selection.as_ref(), Self::visit_condition)
            }
        }
    }

    fn visit_scalar_field(&mut self, _: &ScalarField) -> StatsCollection {
        let mut result = StatsCollection::default();
        result.scalar_field.total += 1;
        result
    }

    fn visit_linked_field(&mut self, field: &LinkedField) -> StatsCollection {
        let mut result = self.visit_selections(&field.selections);
        result.linked_field.total += 1;
        result
    }

    fn visit_condition(&mut self, condition: &Condition) -> StatsCollection {
        let mut result = self.visit_selections(&condition.selections);
        result.condition.total += 1;
        result
    }

    fn visit_inline_fragment(&mut self, fragment: &InlineFragment) -> StatsCollection {
        let mut result = self.visit_selections(&fragment.selections);
        result.inline_fragment.total += 1;
        result
    }

    fn cached_visit_fn<F, T>(&mut self, node: &T, count_fn: F) -> StatsCollection
    where
        F: Fn(&mut Self, &T) -> StatsCollection,
    {
        let address = PointerAddress::new(node);
        let result = self.visited.get(&address);
        if let Some(result) = result {
            return result.to_duplicate();
        }
        let result = count_fn(self, node);
        self.visited.insert(address, result.clone());
        result
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

//! This module is responsible to build a single project. It does not handle
//! watch mode or other state.

mod artifact_generated_types;
pub mod artifact_writer;
mod build_ir;
mod build_resolvers_schema;
pub mod build_schema;
mod generate_artifacts;
pub mod generate_extra_artifacts;
mod log_program_stats;
mod persist_operations;
mod project_asts;
mod source_control;
mod validate;

use std::path::PathBuf;
use std::sync::Arc;

pub use artifact_generated_types::ArtifactGeneratedTypes;
use build_ir::BuildIRResult;
pub use build_ir::SourceHashes;
pub use build_schema::build_schema;
use common::sync::*;
use common::Diagnostic;
use common::PerfLogEvent;
use common::PerfLogger;
use common::WithDiagnostics;
use dashmap::mapref::entry::Entry;
use dashmap::DashSet;
use fnv::FnvBuildHasher;
use fnv::FnvHashMap;
use fnv::FnvHashSet;
pub use generate_artifacts::generate_artifacts;
pub use generate_artifacts::Artifact;
pub use generate_artifacts::ArtifactContent;
use graphql_ir::ExecutableDefinitionName;
use graphql_ir::FragmentDefinitionNameSet;
use graphql_ir::Program;
use log::debug;
use log::info;
use log::warn;
use rayon::iter::IntoParallelRefIterator;
use rayon::slice::ParallelSlice;
use relay_codegen::Printer;
use relay_transforms::apply_transforms;
use relay_transforms::CustomTransformsConfig;
use relay_transforms::Programs;
use relay_typegen::FragmentLocations;
use schema::SDLSchema;
pub use source_control::add_to_mercurial;
pub use validate::validate;
pub use validate::AdditionalValidations;

use self::log_program_stats::print_stats;
pub use self::project_asts::find_duplicates;
pub use self::project_asts::get_project_asts;
pub use self::project_asts::ProjectAstData;
pub use self::project_asts::ProjectAsts;
use super::artifact_content;
use crate::artifact_map::ArtifactMap;
use crate::compiler_state::ArtifactMapKind;
use crate::compiler_state::CompilerState;
use crate::compiler_state::ProjectName;
use crate::config::Config;
use crate::config::ProjectConfig;
use crate::errors::BuildProjectError;
use crate::file_source::SourceControlUpdateStatus;
use crate::graphql_asts::GraphQLAsts;

type BuildProjectOutput = WithDiagnostics<(ProjectName, Arc<SDLSchema>, Programs, Vec<Artifact>)>;
type BuildProgramsOutput = WithDiagnostics<(Programs, Arc<SourceHashes>)>;

pub enum BuildProjectFailure {
    Error(BuildProjectError),
    Cancelled,
}

impl From<BuildProjectError> for BuildProjectFailure {
    fn from(err: BuildProjectError) -> BuildProjectFailure {
        BuildProjectFailure::Error(err)
    }
}

/// This program doesn't have IR transforms applied to it, so it's not optimized.
/// It's perfect for the LSP server: we have all the documents with
/// their locations to provide information to go_to_definition, hover, etc.
pub fn build_raw_program(
    project_config: &ProjectConfig,
    project_asts: ProjectAsts,
    schema: Arc<SDLSchema>,
    log_event: &impl PerfLogEvent,
    is_incremental_build: bool,
) -> Result<(Program, SourceHashes), BuildProjectError> {
    // Build a type aware IR.
    let BuildIRResult { ir, source_hashes } = log_event.time("build_ir_time", || {
        build_ir::build_ir(project_config, project_asts, &schema, is_incremental_build).map_err(
            |errors| BuildProjectError::ValidationErrors {
                errors,
                project_name: project_config.name,
            },
        )
    })?;

    // Turn the IR into a base Program.
    let program = log_event.time("build_program_time", || {
        Program::from_definitions(schema, ir)
    });

    Ok((program, source_hashes))
}

pub fn validate_program(
    config: &Config,
    project_config: &ProjectConfig,
    program: &Program,
    log_event: &impl PerfLogEvent,
) -> Result<Vec<Diagnostic>, BuildProjectError> {
    let timer = log_event.start("validate_time");
    log_event.number("validate_documents_count", program.document_count());
    let result = validate(program, project_config, &config.additional_validations).map_or_else(
        |errors| {
            Err(BuildProjectError::ValidationErrors {
                errors,
                project_name: project_config.name,
            })
        },
        |result| Ok(result.diagnostics),
    );

    log_event.stop(timer);

    result
}

/// Apply various chains of transforms to create a set of output programs.
pub fn transform_program(
    project_config: &ProjectConfig,
    program: Arc<Program>,
    base_fragment_names: Arc<FragmentDefinitionNameSet>,
    perf_logger: Arc<impl PerfLogger + 'static>,
    log_event: &impl PerfLogEvent,
    custom_transforms_config: Option<&CustomTransformsConfig>,
) -> Result<Programs, BuildProjectFailure> {
    let timer = log_event.start("apply_transforms_time");
    let result = apply_transforms(
        project_config,
        program,
        base_fragment_names,
        perf_logger,
        Some(print_stats),
        custom_transforms_config,
    )
    .map_err(|errors| {
        BuildProjectFailure::Error(BuildProjectError::ValidationErrors {
            errors,
            project_name: project_config.name,
        })
    });

    log_event.stop(timer);

    result
}

pub fn build_programs(
    config: &Config,
    project_config: &ProjectConfig,
    compiler_state: &CompilerState,
    project_asts: ProjectAsts,
    base_fragment_names: FragmentDefinitionNameSet,
    schema: Arc<SDLSchema>,
    log_event: &impl PerfLogEvent,
    perf_logger: Arc<impl PerfLogger + 'static>,
) -> Result<BuildProgramsOutput, BuildProjectFailure> {
    let project_name = project_config.name;
    let is_incremental_build = compiler_state.has_processed_changes()
        && !compiler_state.has_breaking_schema_change(project_name, &project_config.schema_config)
        && if let Some(base) = project_config.base {
            !compiler_state.has_breaking_schema_change(base, &project_config.schema_config)
        } else {
            true
        };

    let (program, source_hashes) = build_raw_program(
        project_config,
        project_asts,
        schema,
        log_event,
        is_incremental_build,
    )?;

    if compiler_state.should_cancel_current_build() {
        debug!("Build is cancelled: updates in source code/or new file changes are pending.");
        return Err(BuildProjectFailure::Cancelled);
    }

    // Call validation rules that go beyond type checking.
    // FIXME: Return non-fatal diagnostics from transforms (only validations for now)
    let diagnostics = validate_program(config, project_config, &program, log_event)?;

    let programs = transform_program(
        project_config,
        Arc::new(program),
        Arc::new(base_fragment_names),
        Arc::clone(&perf_logger),
        log_event,
        config.custom_transforms.as_ref(),
    )?;

    Ok(WithDiagnostics {
        item: (programs, Arc::new(source_hashes)),
        diagnostics,
    })
}

pub fn build_project(
    config: &Config,
    project_config: &ProjectConfig,
    compiler_state: &CompilerState,
    graphql_asts_map: &FnvHashMap<ProjectName, GraphQLAsts>,
    perf_logger: Arc<impl PerfLogger + 'static>,
) -> Result<BuildProjectOutput, BuildProjectFailure> {
    let log_event = perf_logger.create_event("build_project");
    let build_time = log_event.start("build_project_time");
    let project_name = project_config.name;
    log_event.string("project", project_name.to_string());
    info!("[{}] compiling...", project_name);

    // Construct a schema instance including project specific extensions.
    let schema = log_event
        .time("build_schema_time", || {
            build_schema(compiler_state, project_config, graphql_asts_map)
        })
        .map_err(|errors| {
            BuildProjectFailure::Error(BuildProjectError::ValidationErrors {
                errors,
                project_name: project_config.name,
            })
        })?;

    let ProjectAstData {
        project_asts,
        base_fragment_names,
    } = get_project_asts(&schema, graphql_asts_map, project_config)?;

    if compiler_state.should_cancel_current_build() {
        debug!("Build is cancelled: updates in source code/or new file changes are pending.");
        return Err(BuildProjectFailure::Cancelled);
    }

    // Apply different transform pipelines to produce the `Programs`.
    let WithDiagnostics {
        item: (programs, source_hashes),
        diagnostics,
    } = build_programs(
        config,
        project_config,
        compiler_state,
        project_asts,
        base_fragment_names,
        Arc::clone(&schema),
        &log_event,
        Arc::clone(&perf_logger),
    )?;

    if compiler_state.should_cancel_current_build() {
        debug!("Build is cancelled: updates in source code/or new file changes are pending.");
        return Err(BuildProjectFailure::Cancelled);
    }

    // Generate artifacts by collecting information from the `Programs`.
    let artifacts_timer = log_event.start("generate_artifacts_time");
    let artifacts = generate_artifacts(
        config,
        project_config,
        &programs,
        Arc::clone(&source_hashes),
    );
    log_event.stop(artifacts_timer);

    log_event.number(
        "generated_artifacts",
        programs.reader.document_count() + programs.normalization.document_count(),
    );

    log_event.stop(build_time);
    log_event.complete();
    Ok(WithDiagnostics {
        item: (project_config.name, schema, programs, artifacts),
        diagnostics,
    })
}

#[allow(clippy::too_many_arguments)]
pub async fn commit_project(
    config: &Config,
    project_config: &ProjectConfig,
    perf_logger: Arc<impl PerfLogger + 'static>,
    schema: &SDLSchema,
    programs: Programs,
    mut artifacts: Vec<Artifact>,
    artifact_map: Arc<ArtifactMapKind>,
    // Definitions that are removed from the previous artifact map
    removed_definition_names: Vec<ExecutableDefinitionName>,
    // Dirty artifacts that should be removed if no longer in the artifacts map
    mut artifacts_to_remove: DashSet<PathBuf, FnvBuildHasher>,
    source_control_update_status: Arc<SourceControlUpdateStatus>,
) -> Result<ArtifactMap, BuildProjectFailure> {
    let log_event = perf_logger.create_event("commit_project");
    log_event.string("project", project_config.name.to_string());
    let commit_time = log_event.start("commit_project_time");

    let fragment_locations = FragmentLocations::new(programs.typegen.fragments());
    if source_control_update_status.is_started() {
        debug!("commit_project cancelled before persisting due to source control updates");
        return Err(BuildProjectFailure::Cancelled);
    }

    if let Some(operation_persister) = config
        .create_operation_persister
        .as_ref()
        .and_then(|create_fn| create_fn(project_config))
    {
        let persist_operations_timer = log_event.start("persist_operations_time");
        persist_operations::persist_operations(
            &mut artifacts,
            &config.root_dir,
            config,
            project_config,
            &(*operation_persister),
            &log_event,
            &programs,
        )
        .await?;
        log_event.stop(persist_operations_timer);
    }

    if source_control_update_status.is_started() {
        debug!(
            "commit_project cancelled before generating extra artifacts due to source control updates"
        );
        return Err(BuildProjectFailure::Cancelled);
    }

    // In some cases we need to create additional (platform specific) artifacts
    // For that, we will use `generate_extra_artifacts` from the configs
    if let Some(generate_extra_artifacts_fn) = &config.generate_extra_artifacts {
        log_event.time("generate_extra_artifacts_time", || {
            artifacts.extend(generate_extra_artifacts_fn(
                project_config,
                schema,
                &programs,
                &artifacts,
            ))
        });
    }

    if source_control_update_status.is_started() {
        debug!("commit_project cancelled before writing artifacts due to source control updates");
        return Err(BuildProjectFailure::Cancelled);
    }

    let should_stop_updating_artifacts = || {
        if source_control_update_status.is_started() {
            debug!("artifact_writer updates cancelled due source control updates");
            true
        } else {
            false
        }
    };

    // Write the generated artifacts to disk. This step is separate from
    // generating artifacts or persisting to avoid partial writes in case of
    // errors as much as possible.
    let next_artifact_map = match Arc::as_ref(&artifact_map) {
        ArtifactMapKind::Unconnected(existing_artifacts) => {
            let mut existing_artifacts = existing_artifacts.clone();
            let write_artifacts_time = log_event.start("write_artifacts_time");
            write_artifacts(
                config,
                project_config,
                schema,
                should_stop_updating_artifacts,
                &artifacts,
                &fragment_locations,
            )?;
            for artifact in &artifacts {
                if !existing_artifacts.remove(&artifact.path) {
                    debug!(
                        "[{}] new artifact {:?} from definitions {:?}",
                        project_config.name, &artifact.path, &artifact.source_definition_names
                    );
                }
            }
            log_event.stop(write_artifacts_time);
            let delete_artifacts_time = log_event.start("delete_artifacts_time");
            for remaining_artifact in &existing_artifacts {
                if should_stop_updating_artifacts() {
                    break;
                }
                let path = config.root_dir.join(remaining_artifact);
                config.artifact_writer.remove(path)?;
            }
            log_event.stop(delete_artifacts_time);
            ArtifactMap::from(artifacts)
        }
        ArtifactMapKind::Mapping(artifact_map) => {
            let artifact_map = artifact_map.clone();
            let current_paths_map = ArtifactMap::default();
            let write_artifacts_incremental_time =
                log_event.start("write_artifacts_incremental_time");

            // Write or update artifacts
            write_artifacts(
                config,
                project_config,
                schema,
                should_stop_updating_artifacts,
                &artifacts,
                &fragment_locations,
            )?;
            artifacts.into_par_iter().for_each(|artifact| {
                current_paths_map.insert(artifact);
            });
            log_event.stop(write_artifacts_incremental_time);

            log_event.time("update_artifact_map_time", || {
                // All generated paths for removed definitions should be removed
                for name in &removed_definition_names {
                    if let Some((_, artifacts)) = artifact_map.0.remove(name) {
                        artifacts_to_remove.extend(artifacts.into_iter().map(|a| a.path));
                    }
                }
                // Update the artifact map, and delete any removed artifacts
                current_paths_map.0.into_par_iter().for_each(
                    |(definition_name, artifact_records)| match artifact_map
                        .0
                        .entry(definition_name)
                    {
                        Entry::Occupied(mut entry) => {
                            let prev_records = entry.get_mut();
                            let current_records_paths =
                                FnvHashSet::from_iter(artifact_records.iter().map(|r| &r.path));

                            for prev_record in prev_records.drain(..) {
                                if !current_records_paths.contains(&prev_record.path) {
                                    artifacts_to_remove.insert(prev_record.path);
                                }
                            }
                            *prev_records = artifact_records;
                        }
                        Entry::Vacant(entry) => {
                            entry.insert(artifact_records);
                        }
                    },
                );
                // Filter out any artifact that is in the artifact map
                if !artifacts_to_remove.is_empty() {
                    artifact_map.0.par_iter().for_each(|entry| {
                        for artifact in entry.value() {
                            artifacts_to_remove.remove(&artifact.path);
                        }
                    });
                }
            });
            let delete_artifacts_incremental_time =
                log_event.start("delete_artifacts_incremental_time");
            // The remaining dirty artifacts are no longer required
            for path in artifacts_to_remove {
                if should_stop_updating_artifacts() {
                    break;
                }
                config.artifact_writer.remove(config.root_dir.join(path))?;
            }
            log_event.stop(delete_artifacts_incremental_time);

            artifact_map
        }
    };

    if source_control_update_status.is_started() {
        log_event.number("update_artifacts_after_source_control_update", 1);
        debug!(
            "We just updated artifacts after source control update happened. Most likely we have outdated artifacts now..."
        );
        warn!(
            r#"
Build canceled due to a source control update while we're writing artifacts.
The compiler may produce outdated artifacts, but it will regenerate the correct set after the update is completed."#
        );
        return Err(BuildProjectFailure::Cancelled);
    } else {
        // For now, lets log how often this is happening, so we can decide if we want to
        // adjust the way we write artifacts. For example, we could write them to the temp
        // directory first, then move to a correct destination.
        log_event.number("update_artifacts_after_source_control_update", 0);
    }

    info!(
        "[{}] compiled documents: {} reader, {} normalization, {} operation text",
        project_config.name,
        programs.reader.document_count(),
        programs.normalization.document_count(),
        programs.operation_text.document_count()
    );
    log_event.stop(commit_time);
    log_event.complete();

    Ok(next_artifact_map)
}

fn write_artifacts<F: Fn() -> bool + Sync + Send>(
    config: &Config,
    project_config: &ProjectConfig,
    schema: &SDLSchema,
    should_stop_updating_artifacts: F,
    artifacts: &[Artifact],
    fragment_locations: &FragmentLocations,
) -> Result<(), BuildProjectFailure> {
    artifacts.par_chunks(8192).try_for_each_init(
        || Printer::with_dedupe(project_config),
        |mut printer, artifacts| {
            for artifact in artifacts {
                if should_stop_updating_artifacts() {
                    return Err(BuildProjectFailure::Cancelled);
                }
                let path = config.root_dir.join(&artifact.path);
                let content = artifact.content.as_bytes(
                    config,
                    project_config,
                    &mut printer,
                    schema,
                    artifact.source_file,
                    fragment_locations,
                );
                if config.artifact_writer.should_write(&path, &content)? {
                    config.artifact_writer.write(path, content)?;
                }
            }
            Ok(())
        },
    )?;
    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::fs;
use std::path::PathBuf;

use common::sync::ParallelIterator;
use common::PerfLogEvent;
use lazy_static::lazy_static;
use log::debug;
use md5::Digest;
use md5::Md5;
use rayon::iter::IntoParallelRefMutIterator;
use regex::Regex;
use relay_codegen::QueryID;
use relay_transforms::Programs;

use crate::config::ArtifactForPersister;
use crate::config::Config;
use crate::config::ProjectConfig;
use crate::errors::BuildProjectError;
use crate::Artifact;
use crate::ArtifactContent;
use crate::OperationPersister;

lazy_static! {
    static ref RELAY_HASH_REGEX: Regex = Regex::new(r#"@relayHash (\w{32})\n"#).unwrap();
    static ref REQUEST_ID_REGEX: Regex = Regex::new(r#"@relayRequestID (.+)\n"#).unwrap();
}

pub async fn persist_operations(
    artifacts: &mut [Artifact],
    root_dir: &PathBuf,
    config: &Config,
    project_config: &ProjectConfig,
    operation_persister: &'_ (dyn OperationPersister + Send + Sync),
    log_event: &impl PerfLogEvent,
    programs: &Programs,
) -> Result<(), BuildProjectError> {
    let handles = artifacts
        .par_iter_mut()
        .flat_map(|artifact| {
            if let ArtifactContent::Operation {
                ref text,
                ref mut id_and_text_hash,
                ref reader_operation,
                ..
            } = artifact.content
            {
                if let Some(Some(virtual_id_file_name)) = config
                    .generate_virtual_id_file_name
                    .as_ref()
                    .map(|gen_name| gen_name(project_config, reader_operation, &programs.reader))
                {
                    if text.is_some() {
                        *id_and_text_hash = Some(QueryID::External(virtual_id_file_name));
                    }
                    None
                } else if let Some(text) = text {
                    let text_hash = md5(text);
                    let artifact_path = root_dir.join(&artifact.path);
                    let relative_path = artifact.path.to_owned();
                    let extracted_persist_id = if config.repersist_operations {
                        None
                    } else {
                        extract_persist_id(&artifact_path, &text_hash)
                    };
                    if let Some(id) = extracted_persist_id {
                        *id_and_text_hash = Some(QueryID::Persisted { id, text_hash });
                        None
                    } else {
                        let text = text.clone();
                        Some(async move {
                            operation_persister
                                .persist_artifact(ArtifactForPersister {
                                    text,
                                    relative_path,
                                })
                                .await
                                .map(|id| {
                                    *id_and_text_hash = Some(QueryID::Persisted { id, text_hash });
                                })
                        })
                    }
                } else {
                    None
                }
            } else {
                None
            }
        })
        .collect::<Vec<_>>();
    log_event.number("persist_documents", handles.len());
    let results = futures::future::join_all(handles).await;
    operation_persister
        .finalize()
        .map_err(|error| BuildProjectError::PersistErrors {
            errors: vec![error],
            project_name: project_config.name,
        })?;
    debug!("done persisting");
    let errors = results
        .into_iter()
        .filter_map(Result::err)
        .collect::<Vec<_>>();
    if !errors.is_empty() {
        let error = BuildProjectError::PersistErrors {
            errors,
            project_name: project_config.name,
        };
        log_event.string("error", error.to_string());
        return Err(error);
    }
    Ok(())
}

fn extract_persist_id(path: &PathBuf, text_hash: &str) -> Option<String> {
    let content = fs::read_to_string(path).ok()?;

    // Looks like a merge conflict, let's not trust this file.
    if content.contains("<<<<") || content.contains(">>>>") {
        return None;
    }

    // If the existing hash doesn't match the hash of the new query test, abort.
    if let Some(existing_hash) = extract_relay_hash(&content) {
        if existing_hash != text_hash {
            return None;
        }
    }

    extract_request_id(&content)
}

fn extract_relay_hash(content: &str) -> Option<&str> {
    RELAY_HASH_REGEX
        .captures(content)
        .and_then(|captures| captures.get(1).map(|m| m.as_str()))
}

fn extract_request_id(content: &str) -> Option<String> {
    REQUEST_ID_REGEX
        .captures(content)
        .map(|captures| captures[1].to_owned())
}

fn md5(data: &str) -> String {
    let mut md5 = Md5::new();
    md5.update(data);
    hex::encode(md5.finalize())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use common::Diagnostic;
use dependency_analyzer::get_reachable_ast;
use dependency_analyzer::ExecutableDefinitionNameSet;
use dependency_analyzer::ReachableAst;
use fnv::FnvHashMap;
use graphql_ir::FragmentDefinitionName;
use graphql_ir::FragmentDefinitionNameSet;
use graphql_ir::OperationDefinitionName;
use graphql_syntax::ExecutableDefinition;
use relay_config::ProjectConfig;
use relay_transforms::get_resolver_fragment_dependency_name;
use schema::SDLSchema;
use schema::Schema;

use crate::compiler_state::ProjectName;
use crate::errors::BuildProjectError;
use crate::GraphQLAsts;

pub struct ProjectAsts {
    pub changed_names: ExecutableDefinitionNameSet,
    pub base_definition_names: ExecutableDefinitionNameSet,
    pub definitions: Vec<ExecutableDefinition>,
}

pub struct ProjectAstData {
    pub project_asts: ProjectAsts,
    pub base_fragment_names: FragmentDefinitionNameSet,
}

pub fn get_project_asts(
    schema: &SDLSchema,
    graphql_asts: &FnvHashMap<ProjectName, GraphQLAsts>,
    project_config: &ProjectConfig,
) -> Result<ProjectAstData, BuildProjectError> {
    let project_asts = graphql_asts
        .get(&project_config.name)
        .map(|asts| asts.get_all_executable_definitions())
        .unwrap_or_default();
    let (base_project_asts, base_definition_names) = match project_config.base {
        Some(base_project_name) => {
            let base_project_asts = graphql_asts
                .get(&base_project_name)
                .map(|asts| asts.get_all_executable_definitions())
                .unwrap_or_default();
            let base_definition_names = base_project_asts
                .iter()
                // TODO(T64459085): Figure out what to do about unnamed (anonymous) operations
                .filter_map(|definition| {
                    definition.name().map(|name| match definition {
                        ExecutableDefinition::Operation(_) => OperationDefinitionName(name).into(),
                        ExecutableDefinition::Fragment(_) => FragmentDefinitionName(name).into(),
                    })
                })
                .collect::<ExecutableDefinitionNameSet>();
            (base_project_asts, base_definition_names)
        }
        None => (Vec::new(), Default::default()),
    };
    find_duplicates(&project_asts, &base_project_asts).map_err(|errors| {
        BuildProjectError::ValidationErrors {
            errors,
            project_name: project_config.name,
        }
    })?;

    let mut base_resolver_fragment_asts =
        find_base_resolver_fragment_asts(schema, &base_definition_names, &base_project_asts);

    let ReachableAst {
        mut definitions,
        mut base_fragment_names,
    } = get_reachable_ast(project_asts, base_project_asts);

    base_fragment_names.extend(
        base_resolver_fragment_asts
            .iter()
            .filter_map(|ast| ast.name().map(FragmentDefinitionName)),
    );
    definitions.append(&mut base_resolver_fragment_asts);

    Ok(ProjectAstData {
        project_asts: ProjectAsts {
            definitions,
            base_definition_names,
            changed_names: find_changed_names(graphql_asts, project_config),
        },
        base_fragment_names,
    })
}

fn find_changed_names(
    graphql_asts: &FnvHashMap<ProjectName, GraphQLAsts>,
    project_config: &ProjectConfig,
) -> ExecutableDefinitionNameSet {
    let mut changed_names = graphql_asts
        .get(&project_config.name)
        .map(|asts| asts.pending_definition_names.clone())
        .unwrap_or_default();
    if let Some(base_project_name) = project_config.base {
        changed_names.extend(
            graphql_asts
                .get(&base_project_name)
                .map(|asts| asts.pending_definition_names.clone())
                .unwrap_or_default(),
        );
    }
    changed_names
}

pub fn find_duplicates(
    asts: &[ExecutableDefinition],
    base_asts: &[ExecutableDefinition],
) -> Result<(), Vec<Diagnostic>> {
    let mut definitions = FnvHashMap::default();

    let mut errors = Vec::new();
    for def in asts.iter().chain(base_asts) {
        if let Some(name) = def.name_identifier() {
            if let Some(prev_def) = definitions.insert(name.value, def) {
                errors.push(
                    Diagnostic::error(
                        graphql_ir::ValidationMessage::DuplicateDefinition(name.value),
                        def.location().with_span(name.span),
                    )
                    .annotate(
                        "previously defined here",
                        prev_def
                            .name_location()
                            .unwrap_or_else(|| prev_def.location()),
                    ),
                );
            }
        }
    }

    if errors.is_empty() {
        Ok(())
    } else {
        Err(errors)
    }
}

/// For all resolver fields defined on the schema
/// this method will return a list of documents from the base projects
fn find_base_resolver_fragment_asts(
    schema: &SDLSchema,
    base_definition_asts: &ExecutableDefinitionNameSet,
    base_project_asts: &[ExecutableDefinition],
) -> Vec<ExecutableDefinition> {
    let mut base_resolver_fragments = ExecutableDefinitionNameSet::default();
    for field in schema.fields() {
        if let Some(fragment_name) = get_resolver_fragment_dependency_name(field) {
            if base_definition_asts.contains(&fragment_name.into()) {
                base_resolver_fragments.insert(fragment_name.into());
            }
        }
    }

    base_project_asts
        .iter()
        .filter(|definition| match definition {
            ExecutableDefinition::Fragment(fragment) => {
                let name = FragmentDefinitionName(fragment.name.value).into();
                base_resolver_fragments.contains(&name)
            }
            ExecutableDefinition::Operation(_) => false,
        })
        .cloned()
        .collect::<Vec<_>>()
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::path::PathBuf;
use std::process::Command;
use std::process::Stdio;
use std::sync::Mutex;

use log::info;

pub fn add_to_mercurial(
    root_dir: &PathBuf,
    added: &Mutex<Vec<PathBuf>>,
    removed: &Mutex<Vec<PathBuf>>,
) -> crate::errors::Result<()> {
    {
        let mut added = added.lock().unwrap();
        if !added.is_empty() {
            for added_files in added.chunks(100) {
                if Command::new("hg")
                    .current_dir(root_dir)
                    .arg("add")
                    .args(added_files)
                    .stdout(Stdio::null())
                    .stderr(Stdio::null())
                    .stdin(Stdio::null())
                    .spawn()
                    .is_err()
                {
                    info!("Failed to run `hg add`.");
                }
            }
            added.clear();
        }
    }
    {
        let mut removed = removed.lock().unwrap();
        if !removed.is_empty() {
            for removed_files in removed.chunks(100) {
                if Command::new("hg")
                    .current_dir(root_dir)
                    .arg("forget")
                    .args(removed_files)
                    .stdout(Stdio::null())
                    .stderr(Stdio::null())
                    .stdin(Stdio::null())
                    .spawn()
                    .is_err()
                {
                    info!("Failed to run `hg forget`.");
                }
            }
            removed.clear();
        }
    }
    Ok(())
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use common::escalate_and_check;
use common::CriticalDiagnostics;
use common::DiagnosticsResult;
use common::FeatureFlags;
use common::StableDiagnostics;
use common::WithDiagnostics;
use errors::try_all;
use graphql_ir::Program;
use relay_config::ProjectConfig;
use relay_transforms::disallow_circular_no_inline_fragments;
use relay_transforms::disallow_reserved_aliases;
use relay_transforms::disallow_typename_on_root;
use relay_transforms::validate_assignable_directive;
use relay_transforms::validate_connections;
use relay_transforms::validate_global_variable_names;
use relay_transforms::validate_module_names;
use relay_transforms::validate_no_double_underscore_alias;
use relay_transforms::validate_no_inline_fragments_with_raw_response_type;
use relay_transforms::validate_no_unselectable_selections;
use relay_transforms::validate_relay_directives;
use relay_transforms::validate_resolver_fragments;
use relay_transforms::validate_static_args;
use relay_transforms::validate_unused_fragment_variables;
use relay_transforms::validate_unused_variables;
use relay_transforms::validate_updatable_directive;
use relay_transforms::validate_updatable_fragment_spread;

pub type AdditionalValidations =
    Box<dyn Fn(&Program, &FeatureFlags) -> DiagnosticsResult<()> + Sync + Send>;

pub fn validate(
    program: &Program,
    project_config: &ProjectConfig,
    additional_validations: &Option<AdditionalValidations>,
) -> DiagnosticsResult<WithDiagnostics<()>> {
    let output = try_all(vec![
        disallow_reserved_aliases(program, &project_config.schema_config),
        validate_no_unselectable_selections(program, &project_config.schema_config),
        validate_no_double_underscore_alias(program),
        validate_unused_variables(program),
        validate_unused_fragment_variables(program),
        validate_connections(program, &project_config.schema_config.connection_interface),
        validate_relay_directives(program),
        validate_global_variable_names(program),
        validate_module_names(program),
        validate_no_inline_fragments_with_raw_response_type(program),
        disallow_typename_on_root(program),
        validate_static_args(program),
        if let Some(ref validate) = additional_validations {
            validate(program, &project_config.feature_flags)
        } else {
            Ok(())
        },
        disallow_circular_no_inline_fragments(program),
        validate_updatable_directive(program),
        validate_updatable_fragment_spread(program),
        validate_assignable_directive(program),
        if project_config.feature_flags.enable_relay_resolver_transform {
            validate_resolver_fragments(program)
        } else {
            Ok(())
        },
    ]);

    match output {
        Ok(_) => Ok(WithDiagnostics {
            item: (),
            diagnostics: Vec::new(),
        }),
        Err(errors) => {
            let critical_level = project_config.diagnostic_report_config.critical_level;

            // We are ignoring the results of successful validations in the error branch, since
            // `try_map` returns a vector of all errors if any validator returned an error.
            // This is okay because successful validations return no special information
            // (i.e. their Ok variant contains ()).

            escalate_and_check(critical_level.into(), errors)
                .map(|StableDiagnostics(diagnostics)| WithDiagnostics {
                    item: (),
                    diagnostics,
                })
                .map_err(|CriticalDiagnostics(errors)| errors)
        }
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::fs::File as FsFile;
use std::io::BufReader;
use std::path::PathBuf;

use common::PerfLogger;

use super::File;
use crate::compiler_state::CompilerState;
use crate::config::Config;
use crate::errors::Error;
use crate::errors::Result;
use crate::FileSourceResult;

/// The purpose of this module is to handle saved state and list of changed files
/// from the external source, and not from the watchman
pub struct ExternalFileSource<'config> {
    changed_files_list: PathBuf,
    pub config: &'config Config,
}

#[derive(Debug)]
pub struct ExternalFileSourceResult {
    pub files: Vec<File>,
    pub resolved_root: PathBuf,
}

impl ExternalFileSourceResult {
    fn read_from_fs(path: &PathBuf, resolved_root: PathBuf) -> Result<Self> {
        let file = FsFile::open(path).map_err(|err| Error::ReadFileError {
            file: path.clone(),
            source: err,
        })?;
        let files: Vec<File> =
            serde_json::from_reader(BufReader::new(file)).map_err(|err| Error::SerdeError {
                file: path.clone(),
                source: err,
            })?;

        Ok(Self {
            files,
            resolved_root,
        })
    }
}

impl<'config> ExternalFileSource<'config> {
    pub fn new(changed_files_list: PathBuf, config: &'config Config) -> Self {
        Self {
            config,
            changed_files_list,
        }
    }

    pub fn create_compiler_state(&self, perf_logger: &impl PerfLogger) -> Result<CompilerState> {
        let load_saved_state_file = self.config.load_saved_state_file.as_ref().unwrap();
        let mut compiler_state = CompilerState::deserialize_from_file(load_saved_state_file)?;
        if std::env::var("RELAY_COMPILER_IGNORE_SAVED_STATE_VERSION").is_err()
            && compiler_state.saved_state_version != self.config.saved_state_version
        {
            return Err(Error::SavedStateVersionMismatch {
                saved_state_version: compiler_state.saved_state_version,
                config_version: self.config.saved_state_version.clone(),
            });
        }

        let root_dir = &self.config.root_dir;
        compiler_state
            .pending_file_source_changes
            .write()
            .unwrap()
            .push(FileSourceResult::External(
                ExternalFileSourceResult::read_from_fs(&self.changed_files_list, root_dir.clone())?,
            ));

        compiler_state.merge_file_source_changes(self.config, perf_logger, true)?;

        Ok(compiler_state)
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::fs;
use std::path::Path;
use std::path::PathBuf;

use common::SourceLocationKey;
use docblock_syntax::DocblockSource;
use extract_graphql::JavaScriptSourceFeature;
use graphql_syntax::GraphQLSource;
use intern::Lookup;
use serde::Deserialize;
use serde::Serialize;

use super::read_file_to_string;
use super::File;
use super::FileSourceResult;
use crate::errors::Result;

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LocatedGraphQLSource {
    pub index: usize,
    pub graphql_source: GraphQLSource,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct LocatedDocblockSource {
    pub index: usize,
    pub docblock_source: DocblockSource,
}

#[derive(Default)]
pub struct LocatedJavascriptSourceFeatures {
    pub graphql_sources: Vec<LocatedGraphQLSource>,
    pub docblock_sources: Vec<LocatedDocblockSource>,
}

pub trait SourceReader {
    fn read_file_to_string(&self, path: &PathBuf) -> std::io::Result<String>;
}

/// Default implementation of the file source reader
/// that is directly using `fs` operations to read the content of the file
pub struct FsSourceReader;

impl SourceReader for FsSourceReader {
    fn read_file_to_string(&self, path: &PathBuf) -> std::io::Result<String> {
        fs::read_to_string(path)
    }
}

/// Reads and extracts `graphql` tagged literals and Relay-specific docblocks
/// from a JavaScript file.
pub fn extract_javascript_features_from_file(
    file_source_result: &FileSourceResult,
    file: &File,
) -> Result<LocatedJavascriptSourceFeatures> {
    let contents = read_file_to_string(file_source_result, file)?;
    let features = extract_graphql::extract(&contents);
    let mut graphql_sources = Vec::new();
    let mut docblock_sources = Vec::new();
    for (index, feature) in features.into_iter().enumerate() {
        match feature {
            JavaScriptSourceFeature::GraphQL(graphql_source) => {
                graphql_sources.push(LocatedGraphQLSource {
                    graphql_source,
                    index,
                })
            }
            JavaScriptSourceFeature::Docblock(docblock_source) => {
                docblock_sources.push(LocatedDocblockSource {
                    docblock_source,
                    index,
                })
            }
        }
    }

    Ok(LocatedJavascriptSourceFeatures {
        graphql_sources,
        docblock_sources,
    })
}

pub fn source_for_location(
    root_dir: &Path,
    source_location: SourceLocationKey,
    source_reader: &dyn SourceReader,
) -> Option<JavaScriptSourceFeature> {
    match source_location {
        SourceLocationKey::Embedded { path, index } => {
            let absolute_path = root_dir.join(path.lookup());
            let contents = source_reader.read_file_to_string(&absolute_path).ok()?;
            let file_sources = extract_graphql::extract(&contents);
            file_sources.into_iter().nth(index.into())
        }
        SourceLocationKey::Standalone { path } => {
            let absolute_path = root_dir.join(path.lookup());
            Some(JavaScriptSourceFeature::GraphQL(GraphQLSource::new(
                source_reader.read_file_to_string(&absolute_path).ok()?,
                0,
                0,
            )))
        }
        SourceLocationKey::Generated => None,
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use core::panic;
use std::borrow::Cow;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;

use common::sync::ParallelIterator;
use fnv::FnvHashSet;
use log::warn;
use rayon::iter::IntoParallelRefIterator;
use relay_typegen::TypegenLanguage;

use super::file_filter::FileFilter;
use super::File;
use super::FileGroup;
use crate::compiler_state::ProjectName;
use crate::compiler_state::ProjectSet;
use crate::config::Config;
use crate::config::SchemaLocation;
use crate::FileSourceResult;

/// The watchman query returns a list of files, but for the compiler we
/// need to categorize these files into multiple groups of files like
/// schema files, extensions and sources by their source set name.
///
/// See `FileGroup` for all groups of files.
pub fn categorize_files(
    config: &Config,
    file_source_result: &FileSourceResult,
) -> HashMap<FileGroup, Vec<File>> {
    let categorizer = FileCategorizer::from_config(config);

    let result = match file_source_result {
        FileSourceResult::Watchman(file_source_result) => {
            let mut relevant_projects = FnvHashSet::default();
            for (project_name, project_config) in &config.projects {
                if project_config.enabled {
                    relevant_projects.insert(project_name);
                    if let Some(base_project) = &project_config.base {
                        relevant_projects.insert(base_project);
                    }
                }
            }
            let has_disabled_projects = relevant_projects.len() < config.projects.len();
            file_source_result
                .files
                .par_iter()
                .map(|file| {
                    let file = File {
                        name: (*file.name).clone(),
                        exists: *file.exists,
                    };
                    let file_group = categorizer.categorize(&file.name).unwrap_or_else(|err| {
                        panic!(
                            "Unexpected error in file categorizer for file `{}`: {}.",
                            file.name.to_string_lossy(),
                            err
                        )
                    });
                    (file_group, file)
                })
                .filter(|(file_group, _)| {
                    !(has_disabled_projects
                        && match &file_group {
                            FileGroup::Generated { project_name: name } => {
                                !relevant_projects.contains(name)
                            }
                            FileGroup::Source { project_set }
                            | FileGroup::Schema { project_set }
                            | FileGroup::Extension { project_set } => !project_set
                                .iter()
                                .any(|name| relevant_projects.contains(name)),
                            FileGroup::Ignore => false,
                        })
                })
                .collect::<Vec<_>>()
        }
        FileSourceResult::External(result) => {
            categorize_non_watchman_files(&categorizer, config, &result.files)
        }
        FileSourceResult::WalkDir(result) => {
            categorize_non_watchman_files(&categorizer, config, &result.files)
        }
    };
    let mut categorized = HashMap::new();
    for (file_group, file) in result {
        categorized
            .entry(file_group)
            .or_insert_with(Vec::new)
            .push(file);
    }
    categorized
}

fn categorize_non_watchman_files(
    categorizer: &FileCategorizer,
    config: &Config,
    files: &[File],
) -> Vec<(FileGroup, File)> {
    let file_filter = FileFilter::from_config(config);

    files
        .par_iter()
        .filter(|file| file_filter.is_file_relevant(&file.name))
        .filter_map(|file| {
            let file_group = categorizer
                .categorize(&file.name)
                .map_err(|err| {
                    warn!(
                        "Unexpected error in file categorizer for file `{}`: {}.",
                        file.name.to_string_lossy(),
                        err
                    );
                    err
                })
                .ok()?;
            Some((file_group, file.clone()))
        })
        .collect::<Vec<_>>()
}

/// The FileCategorizer is created from a Config and categorizes files found by
/// Watchman into what kind of files they are, such as source files of a
/// specific source file group or generated files from some project.
pub struct FileCategorizer {
    source_language: HashMap<ProjectName, TypegenLanguage>,
    extensions_mapping: PathMapping<ProjectSet>,
    default_generated_dir: &'static OsStr,
    generated_dir_mapping: PathMapping<ProjectName>,
    source_mapping: PathMapping<ProjectSet>,
    schema_file_mapping: HashMap<PathBuf, ProjectSet>,
    schema_dir_mapping: PathMapping<ProjectSet>,
}

impl FileCategorizer {
    pub fn from_config(config: &Config) -> Self {
        let mut source_mapping = vec![];
        for (path, project_set) in &config.sources {
            source_mapping.push((path.clone(), project_set.clone()));
        }

        let mut extensions_map: HashMap<PathBuf, ProjectSet> = Default::default();
        for (&project_name, project_config) in &config.projects {
            for extension_dir in &project_config.schema_extensions {
                match extensions_map.entry(extension_dir.clone()) {
                    Entry::Vacant(entry) => {
                        entry.insert(ProjectSet::of(project_name));
                    }
                    Entry::Occupied(mut entry) => {
                        entry.get_mut().insert(project_name);
                    }
                }
            }
        }

        let mut schema_file_mapping: HashMap<PathBuf, ProjectSet> = Default::default();
        for (&project_name, project_config) in &config.projects {
            if let SchemaLocation::File(schema_file) = &project_config.schema_location {
                match schema_file_mapping.entry(schema_file.clone()) {
                    Entry::Vacant(entry) => {
                        entry.insert(ProjectSet::of(project_name));
                    }
                    Entry::Occupied(mut entry) => {
                        entry.get_mut().insert(project_name);
                    }
                }
            }
        }

        let mut schema_dir_mapping_map: HashMap<PathBuf, ProjectSet> = Default::default();
        for (&project_name, project_config) in &config.projects {
            if let SchemaLocation::Directory(directory) = &project_config.schema_location {
                match schema_dir_mapping_map.entry(directory.clone()) {
                    Entry::Vacant(entry) => {
                        entry.insert(ProjectSet::of(project_name));
                    }
                    Entry::Occupied(mut entry) => {
                        entry.get_mut().insert(project_name);
                    }
                }
            }
        }

        let mut schema_dir_mapping = vec![];
        for (path, project_set) in schema_dir_mapping_map {
            schema_dir_mapping.push((path, project_set));
        }

        let mut generated_dir_mapping = Vec::new();
        for (&project_name, project_config) in &config.projects {
            if let Some(output) = &project_config.output {
                generated_dir_mapping.push((output.clone(), project_name));
            }
            if let Some(extra_artifacts_output) = &project_config.extra_artifacts_output {
                generated_dir_mapping.push((extra_artifacts_output.clone(), project_name));
            }
        }
        let source_language: HashMap<ProjectName, TypegenLanguage> = config
            .projects
            .iter()
            .map(|(project_name, project_config)| {
                (*project_name, project_config.typegen_config.language)
            })
            .collect::<HashMap<_, _>>();

        Self {
            source_language,
            extensions_mapping: PathMapping::new(extensions_map.into_iter().collect()),
            default_generated_dir: OsStr::new("__generated__"),
            generated_dir_mapping: PathMapping::new(generated_dir_mapping),
            schema_file_mapping,
            schema_dir_mapping: PathMapping::new(schema_dir_mapping),
            source_mapping: PathMapping::new(source_mapping),
        }
    }

    /// Categorizes a file. This method should be kept as cheap as possible by
    /// preprocessing the config in `from_config` and then re-using the
    /// `FileCategorizer`.
    pub fn categorize(&self, path: &Path) -> Result<FileGroup, Cow<'static, str>> {
        let extension = path.extension();

        if let Some(project_name) = self.generated_dir_mapping.find(path) {
            return if let Some(extension) = extension {
                if is_source_code_extension(extension) || is_extra_extensions(extension) {
                    Ok(FileGroup::Generated { project_name })
                } else {
                    Ok(FileGroup::Ignore)
                }
            } else {
                Ok(FileGroup::Ignore)
            };
        }

        let extension = extension.ok_or(Cow::Borrowed("Got unexpected path without extension."))?;

        if is_source_code_extension(extension) {
            let project_set = self
                .source_mapping
                .find(path)
                .ok_or(Cow::Borrowed("File is not in any source set."))?;
            if self.in_relative_generated_dir(path) {
                if project_set.has_multiple_projects() {
                    Err(Cow::Owned(format!(
                        "Overlapping input sources are incompatible with relative generated \
                        directories. Got file in a relative generated directory with source set {:?}.",
                        project_set,
                    )))
                } else {
                    let project_name = project_set.into_iter().next().unwrap();
                    Ok(FileGroup::Generated { project_name })
                }
            } else {
                let is_valid_extension =
                    self.is_valid_extension_for_project_set(&project_set, extension, path);
                if is_valid_extension {
                    Ok(FileGroup::Source { project_set })
                } else {
                    Err(Cow::Borrowed("Invalid extension for a generated file."))
                }
            }
        } else if is_schema_extension(extension) {
            if let Some(project_set) = self.schema_file_mapping.get(path) {
                Ok(FileGroup::Schema {
                    project_set: project_set.clone(),
                })
            } else if let Some(project_set) = self.extensions_mapping.find(path) {
                Ok(FileGroup::Extension { project_set })
            } else if let Some(project_set) = self.schema_dir_mapping.find(path) {
                Ok(FileGroup::Schema { project_set })
            } else {
                Err(Cow::Borrowed(
                    "Expected *.graphql/*.gql file to be either a schema or extension.",
                ))
            }
        } else {
            Err(Cow::Borrowed(
                "File categorizer encounter a file with unsupported extension.",
            ))
        }
    }

    fn in_relative_generated_dir(&self, path: &Path) -> bool {
        path.components().any(|comp| match comp {
            Component::Normal(comp) => comp == self.default_generated_dir,
            _ => false,
        })
    }

    fn is_valid_extension_for_project_set(
        &self,
        project_set: &ProjectSet,
        extension: &OsStr,
        path: &Path,
    ) -> bool {
        for project_name in project_set.iter() {
            if let Some(language) = self.source_language.get(&project_name) {
                if !is_valid_source_code_extension(language, extension) {
                    warn!(
                        "Unexpected file `{:?}` for language `{:?}`.",
                        path, language
                    );
                    return false;
                }
            }
        }
        true
    }
}

struct PathMapping<T: Clone>(Vec<(PathBuf, T)>);
impl<T: Clone> PathMapping<T> {
    fn new(mut entries: Vec<(PathBuf, T)>) -> Self {
        // Sort so that more specific paths come first, i.e.
        // - foo/bar -> A
        // - foo -> B
        // which ensures we categorize foo/bar/x.js as A.
        entries.sort_by(|(path_a, _), (path_b, _)| path_b.cmp(path_a));
        Self(entries)
    }

    fn find(&self, path: &Path) -> Option<T> {
        self.0.iter().find_map(|(prefix, item)| {
            if path.starts_with(prefix) {
                Some(item.clone())
            } else {
                None
            }
        })
    }
}

fn is_source_code_extension(extension: &OsStr) -> bool {
    extension == "js" || extension == "jsx" || extension == "ts" || extension == "tsx"
}

fn is_schema_extension(extension: &OsStr) -> bool {
    extension == "graphql" || extension == "gql"
}

fn is_extra_extensions(extension: &OsStr) -> bool {
    extension == "php" || extension == "json"
}

fn is_valid_source_code_extension(typegen_language: &TypegenLanguage, extension: &OsStr) -> bool {
    match typegen_language {
        TypegenLanguage::TypeScript => is_source_code_extension(extension),
        TypegenLanguage::Flow | TypegenLanguage::JavaScript => {
            extension == "js" || extension == "jsx"
        }
    }
}

#[cfg(test)]
mod tests {
    use intern::string_key::Intern;

    use super::*;

    fn create_test_config() -> Config {
        Config::from_string_for_test(
            r#"
                {
                    "sources": {
                        "src/js": "public",
                        "src/js/internal": "internal",
                        "src/vendor": "public",
                        "src/custom": "with_custom_generated_dir",
                        "src/typescript": "typescript",
                        "src/custom_overlapping": ["with_custom_generated_dir", "overlapping_generated_dir"]
                    },
                    "projects": {
                        "public": {
                            "schema": "graphql/public.graphql",
                            "language": "flow"
                        },
                        "internal": {
                            "schema": "graphql/__generated__/internal.graphql",
                            "language": "flow"
                        },
                        "with_custom_generated_dir": {
                            "schema": "graphql/__generated__/custom.graphql",
                            "output": "graphql/custom-generated",
                            "language": "flow"
                        },
                        "typescript": {
                            "schema": "graphql/ts_schema.graphql",
                            "language": "typescript"
                        },
                        "overlapping_generated_dir": {
                            "schema": "graphql/__generated__/custom.graphql",
                            "language": "flow"
                        }
                    }
                }
            "#,
        )
        .unwrap()
    }

    #[test]
    fn test_categorize() {
        let config = create_test_config();
        let categorizer = FileCategorizer::from_config(&config);

        assert_eq!(
            categorizer
                .categorize(&PathBuf::from("src/js/a.js"))
                .unwrap(),
            FileGroup::Source {
                project_set: ProjectSet::of("public".intern()),
            },
        );
        assert_eq!(
            categorizer
                .categorize(&PathBuf::from("src/js/nested/b.js"))
                .unwrap(),
            FileGroup::Source {
                project_set: ProjectSet::of("public".intern()),
            },
        );
        assert_eq!(
            categorizer
                .categorize(&PathBuf::from("src/js/internal/nested/c.js"))
                .unwrap(),
            FileGroup::Source {
                project_set: ProjectSet::of("internal".intern()),
            },
        );
        assert_eq!(
            // When custom output dir is provided, path is correctly categorized
            // even if it has same dirname in path as custom output folder.
            // Path is only categorized as generated if it matches the absolute path
            // of the provided custom output.
            categorizer
                .categorize(&PathBuf::from("src/custom/custom-generated/c.js"))
                .unwrap(),
            FileGroup::Source {
                project_set: ProjectSet::of("with_custom_generated_dir".intern()),
            },
        );
        assert_eq!(
            categorizer
                .categorize(&PathBuf::from("src/js/internal/nested/__generated__/c.js"))
                .unwrap(),
            FileGroup::Generated {
                project_name: "internal".intern()
            },
        );
        assert_eq!(
            categorizer
                .categorize(&PathBuf::from("graphql/custom-generated/c.js"))
                .unwrap(),
            FileGroup::Generated {
                project_name: "with_custom_generated_dir".intern()
            },
        );
        assert_eq!(
            categorizer
                .categorize(&PathBuf::from("graphql/public.graphql"))
                .unwrap(),
            FileGroup::Schema {
                project_set: ProjectSet::of("public".intern())
            },
        );
        assert_eq!(
            categorizer
                .categorize(&PathBuf::from("graphql/__generated__/internal.graphql"))
                .unwrap(),
            FileGroup::Schema {
                project_set: ProjectSet::of("internal".intern())
            },
        );
        assert_eq!(
            categorizer
                .categorize(&PathBuf::from("src/typescript/a.ts"))
                .unwrap(),
            FileGroup::Source {
                project_set: ProjectSet::of("typescript".intern()),
            },
        );
    }

    #[test]
    fn test_invalid_extension() {
        let config = create_test_config();
        let categorizer = FileCategorizer::from_config(&config);
        assert_eq!(
            categorizer.categorize(&PathBuf::from("src/js/a.cpp")),
            Err(Cow::Borrowed(
                "File categorizer encounter a file with unsupported extension."
            ))
        );
    }

    #[test]
    fn test_extension_mismatch_panic() {
        let config = create_test_config();
        let categorizer = FileCategorizer::from_config(&config);
        assert_eq!(
            categorizer.categorize(&PathBuf::from("src/js/a.ts")),
            Err(Cow::Borrowed("Invalid extension for a generated file."))
        );
    }

    #[test]
    fn test_categorize_errors() {
        let config = create_test_config();
        let categorizer = FileCategorizer::from_config(&config);

        assert_eq!(
            categorizer.categorize(&PathBuf::from("src/js/a.graphql")),
            Err(Cow::Borrowed(
                "Expected *.graphql/*.gql file to be either a schema or extension."
            )),
        );

        assert_eq!(
            categorizer.categorize(&PathBuf::from("src/js/a.gql")),
            Err(Cow::Borrowed(
                "Expected *.graphql/*.gql file to be either a schema or extension."
            )),
        );

        assert_eq!(
            categorizer.categorize(&PathBuf::from("src/js/noextension")),
            Err(Cow::Borrowed("Got unexpected path without extension.")),
        );

        assert_eq!(
            categorizer.categorize(&PathBuf::from("src/custom_overlapping/__generated__/c.js")),
            Err(Cow::Borrowed(
                "Overlapping input sources are incompatible with relative generated directories. Got file in a relative generated directory with source set ProjectSet([\"with_custom_generated_dir\", \"overlapping_generated_dir\"])."
            )),
        );
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::path::Path;
use std::path::PathBuf;

use fnv::FnvHashSet;
use glob::Pattern;
use intern::string_key::StringKey;

use crate::config::Config;
use crate::config::SchemaLocation;

/// The FileFilter is intended to be used for input sources other than
/// Watchman. The FileFilter is created from `Config` and tries to replicate
/// the filtering done in Watchman. One addition is that this can filter
/// out files from disabled projects.
/// It allows paths defined in `config.sources` with `config.excludes` applied,
/// then allows paths defined for extensions, schemas, and output dirs.
pub struct FileFilter {
    sources_roots: Vec<PathBuf>,
    extra_roots: Vec<PathBuf>,
    excludes: Vec<Pattern>,
}

impl FileFilter {
    pub fn from_config(config: &Config) -> Self {
        let mut enabled_projects_including_base = FnvHashSet::default();
        for project in config.enabled_projects() {
            enabled_projects_including_base.insert(project.name);
            if let Some(base) = project.base {
                enabled_projects_including_base.insert(base);
            }
        }
        Self {
            sources_roots: get_sources_root(config, &enabled_projects_including_base),
            extra_roots: get_extra_roots(config, &enabled_projects_including_base),
            excludes: config
                .excludes
                .iter()
                .map(|p| Pattern::new(p).unwrap())
                .collect(),
        }
    }

    pub fn is_file_relevant(&self, path: &Path) -> bool {
        self.extra_roots.iter().any(|root| path.starts_with(root))
            || (self.sources_roots.iter().any(|root| path.starts_with(root))
                && !self
                    .excludes
                    .iter()
                    .any(|exclude| exclude.matches_path(path)))
    }
}

// Get roots for extensions, schemas and output dirs
fn get_extra_roots(config: &Config, enabled_projects: &FnvHashSet<StringKey>) -> Vec<PathBuf> {
    let mut roots = vec![];
    for project_config in config.projects.values() {
        if !enabled_projects.contains(&project_config.name) {
            continue;
        }
        roots.extend(&project_config.schema_extensions);
        if let Some(output_dir) = &project_config.output {
            roots.push(output_dir);
        }
        if let Some(output_dir) = &project_config.extra_artifacts_output {
            roots.push(output_dir);
        }
        match &project_config.schema_location {
            SchemaLocation::File(path) | SchemaLocation::Directory(path) => roots.push(path),
        }
    }
    unify_roots(roots)
}

fn get_sources_root(config: &Config, enabled_projects: &FnvHashSet<StringKey>) -> Vec<PathBuf> {
    unify_roots(
        config
            .sources
            .iter()
            .filter_map(|(path, project_set)| {
                let is_enabled = project_set
                    .iter()
                    .any(|name| enabled_projects.contains(name));
                is_enabled.then(|| path)
            })
            .collect(),
    )
}

// A copy of watchman_query_builder::unify_roots, but takes Vec<&PathBuf> as the input
fn unify_roots(mut paths: Vec<&PathBuf>) -> Vec<PathBuf> {
    paths.sort();
    let mut roots = Vec::new();
    for path in paths {
        match roots.last() {
            Some(prev) if path.starts_with(&prev) => {
                // skip
            }
            _ => {
                roots.push(path.clone());
            }
        }
    }
    roots
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use crate::compiler_state::ProjectName;
use crate::compiler_state::ProjectSet;

#[derive(Debug, PartialEq, Eq, Hash)]
pub enum FileGroup {
    Generated {
        project_name: ProjectName,
    },
    Schema {
        project_set: ProjectSet,
    },
    Extension {
        project_set: ProjectSet,
    },
    Source {
        project_set: ProjectSet,
    },
    /// Files, that are located in the generated directory, but not created
    /// by relay compiler (utility files: .gitkeep, README, etc.) and should
    /// be ignored
    Ignore,
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

mod external_file_source;
mod extract_graphql;
mod file_categorizer;
mod file_filter;
mod file_group;
mod read_file_to_string;
mod source_control_update_status;
mod walk_dir_file_source;
mod watchman_file_source;
mod watchman_query_builder;

use std::path::PathBuf;

use common::PerfLogEvent;
use common::PerfLogger;
use external_file_source::ExternalFileSource;
pub use file_categorizer::categorize_files;
pub use file_categorizer::FileCategorizer;
pub use file_group::FileGroup;
use graphql_watchman::WatchmanFileSourceResult;
use graphql_watchman::WatchmanFileSourceSubscription;
use graphql_watchman::WatchmanFileSourceSubscriptionNextChange;
use log::warn;
pub use read_file_to_string::read_file_to_string;
use serde::Deserialize;
use serde_bser::value::Value;
pub use source_control_update_status::SourceControlUpdateStatus;
pub use watchman_client::prelude::Clock;
use watchman_file_source::WatchmanFileSource;

pub use self::external_file_source::ExternalFileSourceResult;
pub use self::extract_graphql::extract_javascript_features_from_file;
pub use self::extract_graphql::source_for_location;
pub use self::extract_graphql::FsSourceReader;
pub use self::extract_graphql::LocatedDocblockSource;
pub use self::extract_graphql::LocatedGraphQLSource;
pub use self::extract_graphql::LocatedJavascriptSourceFeatures;
pub use self::extract_graphql::SourceReader;
use self::walk_dir_file_source::WalkDirFileSource;
use self::walk_dir_file_source::WalkDirFileSourceResult;
use crate::compiler_state::CompilerState;
use crate::config::Config;
use crate::config::FileSourceKind;
use crate::errors::Error;
use crate::errors::Result;

pub enum FileSource<'config> {
    Watchman(WatchmanFileSource<'config>),
    External(ExternalFileSource<'config>),
    WalkDir(WalkDirFileSource<'config>),
}

impl<'config> FileSource<'config> {
    pub async fn connect(
        config: &'config Config,
        perf_logger_event: &impl PerfLogEvent,
    ) -> Result<FileSource<'config>> {
        match &config.file_source_config {
            FileSourceKind::Watchman => Ok(Self::Watchman(
                WatchmanFileSource::connect(config, perf_logger_event).await?,
            )),
            FileSourceKind::External(changed_files_list) => Ok(Self::External(
                ExternalFileSource::new(changed_files_list.to_path_buf(), config),
            )),
            FileSourceKind::WalkDir => Ok(Self::WalkDir(WalkDirFileSource::new(config))),
        }
    }

    pub async fn query(
        &self,
        perf_logger_event: &impl PerfLogEvent,
        perf_logger: &impl PerfLogger,
    ) -> Result<CompilerState> {
        match self {
            Self::Watchman(file_source) => file_source.query(perf_logger_event, perf_logger).await,
            Self::External(file_source) => {
                let result = file_source.create_compiler_state(perf_logger);
                if let Err(err) = &result {
                    perf_logger_event.string(
                        "external_file_source_create_compiler_state_error",
                        format!("{:?}", err),
                    );
                    warn!(
                        "Unable to create state from external source: {:?}. Sending a full watchman query...",
                        err
                    );
                    let watchman_file_source =
                        WatchmanFileSource::connect(file_source.config, perf_logger_event).await?;
                    watchman_file_source
                        .full_query(perf_logger_event, perf_logger)
                        .await
                } else {
                    result
                }
            }
            Self::WalkDir(file_source) => file_source.create_compiler_state(perf_logger),
        }
    }

    pub async fn subscribe(
        self,
        perf_logger_event: &impl PerfLogEvent,
        perf_logger: &impl PerfLogger,
    ) -> Result<(CompilerState, FileSourceSubscription)> {
        match self {
            Self::Watchman(file_source) => {
                let (compiler_state, watchman_subscription) = file_source
                    .subscribe(perf_logger_event, perf_logger)
                    .await?;
                Ok((
                    compiler_state,
                    FileSourceSubscription::Watchman(watchman_subscription),
                ))
            }
            Self::External(_) | Self::WalkDir(_) => {
                unimplemented!(
                    "watch-mode (subscribe) is not available for non-watchman file sources."
                )
            }
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct File {
    pub name: PathBuf,
    pub exists: bool,
}

impl File {
    pub fn absolute_path(&self, resolved_root: PathBuf) -> PathBuf {
        let mut absolute_path = resolved_root;
        absolute_path.push(&self.name);
        absolute_path
    }
}

#[derive(Debug)]
pub enum FileSourceResult {
    Watchman(WatchmanFileSourceResult),
    External(ExternalFileSourceResult),
    WalkDir(WalkDirFileSourceResult),
}

impl FileSourceResult {
    pub fn clock(&self) -> Option<Clock> {
        match self {
            Self::Watchman(file_source) => Some(file_source.clock.clone()),
            Self::External(_) => None,
            Self::WalkDir(_) => None,
        }
    }

    pub fn resolved_root(&self) -> PathBuf {
        match self {
            Self::Watchman(file_source_result) => file_source_result.resolved_root.path(),
            Self::External(file_source_result) => file_source_result.resolved_root.clone(),
            Self::WalkDir(file_source_result) => file_source_result.resolved_root.clone(),
        }
    }

    pub fn saved_state_info(&self) -> &Option<Value> {
        match self {
            Self::Watchman(file_source_result) => &file_source_result.saved_state_info,
            Self::External(_) => unimplemented!(),
            Self::WalkDir(_) => unimplemented!(),
        }
    }

    pub fn size(&self) -> usize {
        match self {
            Self::Watchman(file_source_result) => file_source_result.files.len(),
            Self::External(file_source_result) => file_source_result.files.len(),
            Self::WalkDir(file_source_result) => file_source_result.files.len(),
        }
    }
}

pub enum FileSourceSubscription {
    Watchman(WatchmanFileSourceSubscription), // Oss(OssFileSourceSubscription)
}

impl FileSourceSubscription {
    pub async fn next_change(&mut self) -> Result<FileSourceSubscriptionNextChange> {
        match self {
            Self::Watchman(file_source_subscription) => {
                file_source_subscription.next_change().await.map_or_else(
                    |err| Err(Error::from(err)),
                    |next_change| Ok(FileSourceSubscriptionNextChange::Watchman(next_change)),
                )
            }
        }
    }
}

#[derive(Debug)]
pub enum FileSourceSubscriptionNextChange {
    Watchman(WatchmanFileSourceSubscriptionNextChange),
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use super::File;
use super::FileSourceResult;
use crate::errors::Error;
use crate::errors::Result;

/// Reads a file into a string.
pub fn read_file_to_string(file_source_result: &FileSourceResult, file: &File) -> Result<String> {
    assert!(
        file.exists,
        "Can't read from non-existent file: {:?}",
        &file.name
    );
    let absolute_path = file.absolute_path(file_source_result.resolved_root());
    std::fs::read_to_string(&absolute_path).map_err(|err| Error::FileRead {
        file: absolute_path.clone(),
        source: err,
    })
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::sync::atomic::AtomicI8;
use std::sync::atomic::Ordering;
use std::sync::Arc;

#[derive(Clone, Default, Debug)]
/// This structure is representing the state of the current source control update.
/// Watchman subscription will trigger updates here
pub struct SourceControlUpdateStatus {
    // default - no updates
    // 1 - update started
    // 2 - update completed
    value: Arc<AtomicI8>,
}

impl SourceControlUpdateStatus {
    pub fn mark_as_started(&self) {
        self.value.store(1, Ordering::Relaxed);
    }
    pub fn is_started(&self) -> bool {
        self.value.load(Ordering::Relaxed) == 1
    }
    pub fn mark_as_completed(&self) {
        self.value.store(2, Ordering::Relaxed);
    }
    pub fn is_completed(&self) -> bool {
        self.value.load(Ordering::Relaxed) == 2
    }
    pub fn set_to_default(&self) {
        self.value.store(Default::default(), Ordering::Relaxed);
    }
}


/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;

use common::PerfLogEvent;
use common::PerfLogger;
use log::debug;
use relay_typegen::TypegenLanguage;
use walkdir::WalkDir;

use super::File;
use crate::compiler_state::CompilerState;
use crate::config::Config;
use crate::errors::Result;
use crate::FileSourceResult;

#[derive(Debug)]
pub struct WalkDirFileSourceResult {
    pub files: Vec<File>,
    pub resolved_root: PathBuf,
}

pub struct WalkDirFileSource<'config> {
    pub config: &'config Config,
    expected_file_extensions: HashSet<&'config str>,
}

fn get_expected_file_extensions(config: &Config) -> HashSet<&str> {
    let mut file_extensions = HashSet::<&str>::with_capacity(6);
    file_extensions.insert("graphql");
    file_extensions.insert("gql");

    for project in config.enabled_projects() {
        match project.typegen_config.language {
            TypegenLanguage::Flow | TypegenLanguage::JavaScript => {
                file_extensions.insert("js");
                file_extensions.insert("jsx");
            }
            TypegenLanguage::TypeScript => {
                file_extensions.insert("js");
                file_extensions.insert("jsx");
                file_extensions.insert("ts");
                file_extensions.insert("tsx");
            }
        }
    }
    file_extensions
}

impl<'config> WalkDirFileSource<'config> {
    pub fn new(config: &'config Config) -> Self {
        debug!(
            "Watchman server is disabled, or not available. Using GlobFileSource to find files."
        );
        Self {
            config,
            expected_file_extensions: get_expected_file_extensions(config),
        }
    }

    fn should_include_file(&self, name: &Path) -> bool {
        matches!(
            name.extension().map(|extension| self
                .expected_file_extensions
                .contains(extension.to_str().unwrap())),
            Some(true)
        )
    }

    fn find_files(&self) -> Vec<File> {
        WalkDir::new(self.config.root_dir.clone())
            .into_iter()
            .filter_map(|entry| {
                let dir_entry = entry.ok()?;
                let relative_path = dir_entry
                    .path()
                    .strip_prefix(self.config.root_dir.clone())
                    .ok()?
                    .to_path_buf();

                self.should_include_file(&relative_path).then(|| File {
                    name: relative_path,
                    exists: true,
                })
            })
            .collect::<Vec<_>>()
    }

    pub fn create_compiler_state(&self, perf_logger: &impl PerfLogger) -> Result<CompilerState> {
        let setup_event = perf_logger.create_event("Glob_file_source_create_compiler_state");
        let timer = setup_event.start("create_compiler_state_file_files");
        let file_source_changes = FileSourceResult::WalkDir(WalkDirFileSourceResult {
            files: self.find_files(),
            resolved_root: self.config.root_dir.clone(),
        });
        setup_event.stop(timer);
        let compiler_state = CompilerState::from_file_source_changes(
            self.config,
            &file_source_changes,
            &setup_event,
            perf_logger,
        )?;

        setup_event.complete();

        Ok(compiler_state)
    }
}