<NME> lsp_state_resources.rs
<BEF> /*
 * 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::Arc;
use std::sync::RwLock;

use common::PerfLogEvent;
use common::PerfLogger;
use dashmap::mapref::entry::Entry;
use fnv::FnvHashMap;
use graphql_ir::FragmentDefinitionNameSet;
use graphql_watchman::WatchmanFileSourceSubscriptionNextChange;
use intern::string_key::StringKey;
use log::debug;
use rayon::iter::ParallelIterator;
use relay_compiler::build_project::get_project_asts;
use relay_compiler::build_project::ProjectAstData;
use relay_compiler::build_project::ProjectAsts;
use relay_compiler::build_raw_program;
use relay_compiler::build_schema;
use relay_compiler::compiler_state::CompilerState;
use relay_compiler::compiler_state::ProjectName;
use relay_compiler::config::ProjectConfig;
use relay_compiler::errors::BuildProjectError;
use relay_compiler::errors::Error;
use relay_compiler::transform_program;
use relay_compiler::validate_program;
use relay_compiler::BuildProjectFailure;
use relay_compiler::FileSource;
use relay_compiler::FileSourceResult;
use relay_compiler::FileSourceSubscription;
use relay_compiler::FileSourceSubscriptionNextChange;
use relay_compiler::GraphQLAsts;
use relay_compiler::SourceControlUpdateStatus;
use schema::SDLSchema;
use schema_documentation::SchemaDocumentation;
use tokio::task;
use tokio::task::JoinHandle;

use super::lsp_state::ProjectStatus;
use super::lsp_state::Task;
use crate::status_updater::set_ready_status;
use crate::status_updater::update_in_progress_status;
use crate::LSPState;

/// This structure is responsible for keeping schemas/programs in sync with the current state of the world
pub(crate) struct LSPStateResources<
    TPerfLogger: PerfLogger + 'static,
    TSchemaDocumentation: SchemaDocumentation + 'static,
> {
    lsp_state: Arc<LSPState<TPerfLogger, TSchemaDocumentation>>,
}

const MAX_ERROR_COUNT: usize = 3;

impl<TPerfLogger: PerfLogger + 'static, TSchemaDocumentation: SchemaDocumentation + 'static>
    LSPStateResources<TPerfLogger, TSchemaDocumentation>
{
    pub(crate) fn new(lsp_state: Arc<LSPState<TPerfLogger, TSchemaDocumentation>>) -> Self {
        Self { lsp_state }
    }

    pub(crate) fn watch(self) {
        tokio::spawn(async move {
            // Wait for a notify from VSCode document events. This is for preventing
            // starting the loop when the current workspace doesn't have any Relay file.
            self.lsp_state.notify_lsp_state_resources.notified().await;
            self.internal_watch().await;
        });
    }

    /// Create an end-less loop of keeping the resources up-to-date with the source control changes
    async fn internal_watch(&self) {
        // avoid dead loop when watchman has an error
        let mut error_count = 0;
        'outer: loop {
            debug!("Initializing resources for LSP server");

            update_in_progress_status(
                "Relay: watchman...",
                Some("Sending watchman query to get source files and possible saved state"),
                &self.lsp_state.sender,
            );
            let setup_event = self
                .lsp_state
                .perf_logger
                .create_event("lsp_state_initialize_resources");
            let timer = setup_event.start("lsp_state_initialize_resources_time");

            let file_source = match FileSource::connect(&self.lsp_state.config, &setup_event).await
            {
                Ok(f) => f,
                Err(error) => {
                    self.log_errors("watch_build_error", &error);
                    error_count += 1;
                    if error_count == MAX_ERROR_COUNT {
                        panic!("{}", error);
                    }
                    continue;
                }
            };
            let (mut compiler_state, file_source_subscription) = match file_source
                .subscribe(&setup_event, self.lsp_state.perf_logger.as_ref())
                .await
            {
                Ok(f) => f,
                Err(error) => {
                    self.log_errors("watch_build_error", &error);
                    error_count += 1;
                    if error_count == MAX_ERROR_COUNT {
                        panic!("{}", error);
                    }
            self.diagnostic_reporter.clear_regular_diagnostics();

            // Run initial build, before entering the watch changes loop
            if let Err(err) = self.initial_build(&compiler_state, &setup_event) {
                self.report_error(err);
            }
            compiler_state.complete_compilation();

            setup_event.stop(timer);
            self.perf_logger.complete_event(setup_event);
            // If watchman detects the source control update, it will update the state
            // of the source_control_update_status, so we can cancel building schemas/
            // and programs earlier
            let subscription_handle = self.watchman_subscription_handler(
                file_source_subscription,
                pending_file_source_changes,
                Arc::clone(&compiler_state.source_control_update_status),
            );
            update_in_progress_status(
                "Relay: creating state...",
                Some("Building schemas and source programs for LSP"),
                &self.lsp_state.sender,
            );

            self.lsp_state
                .diagnostic_reporter
                .clear_regular_diagnostics();

            // Run initial build, before entering the watch changes loop
            if let Err(error) = self.build_projects(&mut compiler_state, &setup_event) {
                self.publish_errors(&error, "lsp_state_error");
            }
            set_ready_status(&self.lsp_state.sender);

            setup_event.stop(timer);
            setup_event.complete();

            debug!("LSP server initialization completed!");
            error_count = 0;

            // Here we will wait for changes from watchman
            'inner: loop {
                // Wait for a notify from watchman updates, or when a Relay file
                // from an unactivated project is opened in VSCode
                self.lsp_state.notify_lsp_state_resources.notified().await;

                // Source control update started, we can ignore all pending changes, and wait for it to complete,
                // we may change the status bar to `Source Control Update...`
                if compiler_state.source_control_update_status.is_started() {
                    update_in_progress_status(
                        "Relay: hg update...",
                        Some("Waiting for source control update"),
                        &self.lsp_state.sender,
                    );
                    continue 'inner;
                }

                // SC Update completed, we need to abort current subscription, and re-initialize resource for LSP
                if compiler_state.source_control_update_status.is_completed() {
                    debug!("Watchman indicated the the source control update has completed!");
                    subscription_handle.abort();
                    continue 'outer;
                }

                let log_event = self
                    .lsp_state
                    .perf_logger
                    .create_event("lsp_state_watchman_event");
                let log_time = log_event.start("lsp_state_watchman_event_time");

                if let Err(error) = self.incremental_build(&mut compiler_state, &log_event) {
                    self.publish_errors(&error, "lsp_state_user_error");
                }
                set_ready_status(&self.lsp_state.sender);

                log_event.stop(log_time);
                log_event.complete();
            }
        }
    }

    fn incremental_build(
        &self,
        compiler_state: &mut CompilerState,
        log_event: &impl PerfLogEvent,
    ) -> Result<(), Error> {
        let has_new_changes = compiler_state.merge_file_source_changes(
            &self.lsp_state.config,
            self.lsp_state.perf_logger.as_ref(),
            false,
        )?;

    fn initial_build(
        &self,
        compiler_state: &CompilerState,
        log_event: &impl PerfLogEvent,
    ) -> Result<(), Error> {
        debug!("Initial build started...");
                .any(|r| r.value() == &ProjectStatus::Activated)
        {
        // if update detected
        self.build_schemas(compiler_state, log_event)?;
        self.build_source_programs(compiler_state, log_event)?;

        Ok(())
    }
                "Relay: checking...",
                Some("Validating changes, and updating source programs with the latest changes."),
                &self.lsp_state.sender,
            );

            self.build_projects(compiler_state, log_event)?;
        }

        Ok(())
    }

    fn build_projects(
        &self,
        compiler_state: &mut CompilerState,
        log_event: &impl PerfLogEvent,
    ) -> Result<(), Error> {
        let graphql_asts = log_event.time("parse_sources_time", || {
            GraphQLAsts::from_graphql_sources_map(
                &compiler_state.graphql_sources,
                &compiler_state.get_dirty_definitions(&self.lsp_state.config),
            )
        })?;

        if compiler_state.should_cancel_current_build() {
            debug!("Build is cancelled: new file changes are pending.");
            return Err(Error::Cancelled);
        }

        let timer = log_event.start("build_lsp_projects");
        let build_results: Vec<_> = self
            .lsp_state
            .config
            .par_enabled_projects()
            .filter(|project_config| {
                // Filter inactive projects
                if !self
                    .lsp_state
                    .project_status
                    .contains_key(&project_config.name)
                {
                    return false;
                }
                // When the source programs is empty, we need to compile all source programs once
                if !self
                    .lsp_state
                    .source_programs
                    .contains_key(&project_config.name)
                {
                    return true;
                }
                if let Some(base) = project_config.base {
                    if compiler_state.project_has_pending_changes(base) {
                        return true;
                    }
                }
                compiler_state.project_has_pending_changes(project_config.name)
            })
            .map(|project_config| self.build_project(project_config, compiler_state, &graphql_asts))
            .collect();
        log_event.stop(timer);

        let mut errors = vec![];
        for build_result in build_results {
                .read()
                .expect("LSPState::build_in_watch_mode: expect to acquire read lock on schemas"),
            log_event,
        )?;

        self.validate_programs(&programs, base_fragment_names_map, log_event)?;
    }

    fn build_project(
        &self,
        project_config: &ProjectConfig,
        compiler_state: &CompilerState,
        graphql_asts_map: &FnvHashMap<ProjectName, GraphQLAsts>,
    ) -> Result<StringKey, BuildProjectFailure> {
        self.lsp_state
            .project_status
            .insert(project_config.name, ProjectStatus::Completed);
        let log_event = self.lsp_state.perf_logger.create_event("build_lsp_project");
        let project_name = project_config.name;
        let build_time = log_event.start("build_lsp_project_time");
        log_event.string("project", project_name.to_string());

        let schema = log_event.time("build_schema_time", || {
            self.build_schema(compiler_state, project_config, graphql_asts_map)
        })?;

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

        // This will kick-off the validation for all synced sources
        self.lsp_state.schedule_task(Task::ValidateSyncedSources);

        self.build_programs(
            project_config,
            project_asts,
            base_fragment_names,
            compiler_state,
            graphql_asts_map,
            schema,
            &log_event,
        )?;

        log_event.stop(build_time);
        Ok(project_name)
    }

    fn build_schema(
        &self,
        compiler_state: &CompilerState,
        project_config: &ProjectConfig,
        graphql_asts_map: &FnvHashMap<ProjectName, GraphQLAsts>,
    ) -> Result<Arc<SDLSchema>, BuildProjectFailure> {
        match self.lsp_state.schemas.entry(project_config.name) {
            Entry::Vacant(e) => {
                let schema = build_schema(compiler_state, project_config, graphql_asts_map)
                    .map_err(|errors| {
                        BuildProjectFailure::Error(BuildProjectError::ValidationErrors {
                            errors,
                            project_name: project_config.name,
                        })
                    })?;
                e.insert(Arc::clone(&schema));
                Ok(schema)
            }
            Entry::Occupied(mut e) => {
                if !compiler_state.project_has_pending_schema_changes(project_config.name) {
                    Ok(Arc::clone(e.get()))
                } else {
                    let schema = build_schema(compiler_state, project_config, graphql_asts_map)
                        .map_err(|errors| {
                            debug!("build error");
                            BuildProjectFailure::Error(BuildProjectError::ValidationErrors {
                                errors,
                                project_name: project_config.name,
                            })
                        })?;
                    e.insert(Arc::clone(&schema));
                    Ok(schema)
                }
            }
        }
    }

    fn build_programs(
        &self,
        project_config: &ProjectConfig,
        project_asts: ProjectAsts,
        base_fragment_names: FragmentDefinitionNameSet,
        compiler_state: &CompilerState,
        graphql_asts: &FnvHashMap<ProjectName, GraphQLAsts>,
        schema: Arc<SDLSchema>,
        log_event: &impl PerfLogEvent,
    ) -> Result<(), BuildProjectFailure> {
        let is_incremental_build = self
            .lsp_state
            .source_programs
            .contains_key(&project_config.name)
            && compiler_state.has_processed_changes()
            && !compiler_state
                .has_breaking_schema_change(project_config.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 (base_program, _) = 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);
        }

        match self.lsp_state.source_programs.entry(project_config.name) {
            Entry::Vacant(e) => {
                e.insert(base_program.clone());
            }
            Entry::Occupied(mut e) => {
                let program = e.get_mut();
                let removed_definition_names = graphql_asts
                    .get(&project_config.name)
                    .map(|ast| ast.removed_definition_names.as_ref());
                program.merge_program(&base_program, removed_definition_names);
            }
        }

        // Call validation rules that go beyond type checking.
        validate_program(
            &self.lsp_state.config,
            project_config,
            &base_program,
            log_event,
        )?;

        transform_program(
            project_config,
            Arc::new(base_program),
            Arc::new(base_fragment_names),
            Arc::clone(&self.lsp_state.perf_logger),
            log_event,
            self.lsp_state.config.custom_transforms.as_ref(),
        )?;
        Ok(())
    }

    fn watchman_subscription_handler(
        &self,
        mut file_source_subscription: FileSourceSubscription,
        pending_file_source_changes: Arc<RwLock<Vec<FileSourceResult>>>,
        source_code_update_status: Arc<SourceControlUpdateStatus>,
    ) -> JoinHandle<()> {
        let notify_sender = self.lsp_state.notify_lsp_state_resources.clone();
        task::spawn(async move {
            loop {
                match file_source_subscription.next_change().await {
                    Err(_) => {
                        // do nothing? compiler should panic, and restart the lsp
                    }
                    Ok(FileSourceSubscriptionNextChange::Watchman(watchman_next_change)) => {
                        match watchman_next_change {
                            WatchmanFileSourceSubscriptionNextChange::None => {}
                            WatchmanFileSourceSubscriptionNextChange::SourceControlUpdateEnter => {
                                source_code_update_status.mark_as_started();
                            }
                            WatchmanFileSourceSubscriptionNextChange::SourceControlUpdateLeave => {
                                source_code_update_status.set_to_default();
                            }
                            WatchmanFileSourceSubscriptionNextChange::SourceControlUpdate => {
                                source_code_update_status.mark_as_completed();
                                notify_sender.notify_one();
                                break;
                            }
                            WatchmanFileSourceSubscriptionNextChange::Result(
                                file_source_changes,
                            ) => {
                                pending_file_source_changes
                                .write()
                                .expect("LSPState::watch_and_update_schemas: expect to acquire write lock on pending_file_source_changes")
                                .push(FileSourceResult::Watchman(file_source_changes));

                                notify_sender.notify_one();
                            }
                        }
                    }
                }
            }
        })
    }

    fn log_errors(&self, log_event_name: &'static str, error: &Error) {
        let error_event = self.lsp_state.perf_logger.create_event(log_event_name);
        error_event.string("error", error.to_string());
        error_event.complete();
    }

    /// Log errors and report the diagnostics to IDE
    fn publish_errors(&self, error: &Error, log_event_name: &'static str) {
        self.lsp_state.diagnostic_reporter.report_error(error);
        self.lsp_state.diagnostic_reporter.commit_diagnostics();
        self.log_errors(log_event_name, error)
    }
}

<MSG> Fix building full programs on initialization

Reviewed By: alunyov

Differential Revision: D26936595

fbshipit-source-id: 735e045dd8f123ad50a46c966877aef629b92e87

<DFF> @@ -117,10 +117,9 @@ impl<TPerfLogger: PerfLogger + 'static> LSPStateResources<TPerfLogger> {
             self.diagnostic_reporter.clear_regular_diagnostics();
 
             // Run initial build, before entering the watch changes loop
-            if let Err(err) = self.initial_build(&compiler_state, &setup_event) {
+            if let Err(err) = self.initial_build(&mut compiler_state, &setup_event) {
                 self.report_error(err);
             }
-            compiler_state.complete_compilation();
 
             setup_event.stop(timer);
             self.perf_logger.complete_event(setup_event);
@@ -208,7 +207,7 @@ impl<TPerfLogger: PerfLogger + 'static> LSPStateResources<TPerfLogger> {
 
     fn initial_build(
         &self,
-        compiler_state: &CompilerState,
+        compiler_state: &mut CompilerState,
         log_event: &impl PerfLogEvent,
     ) -> Result<(), Error> {
         debug!("Initial build started...");
@@ -217,6 +216,7 @@ impl<TPerfLogger: PerfLogger + 'static> LSPStateResources<TPerfLogger> {
         // if update detected
         self.build_schemas(compiler_state, log_event)?;
         self.build_source_programs(compiler_state, log_event)?;
+        compiler_state.complete_compilation();
 
         Ok(())
     }
@@ -286,6 +286,13 @@ impl<TPerfLogger: PerfLogger + 'static> LSPStateResources<TPerfLogger> {
                 .read()
                 .expect("LSPState::build_in_watch_mode: expect to acquire read lock on schemas"),
             log_event,
+            // When the source programs is empty, we need to compile all source programs once
+            self.source_programs
+                .read()
+                .expect(
+                    "LSPState::build_in_watch_mode: expect to acquire read lock on source_programs",
+                )
+                .is_empty(),
         )?;
 
         self.validate_programs(&programs, base_fragment_names_map, log_event)?;

{'commit': 'diff --git a/compiler/crates/relay-compiler/src/compiler.rs b/compiler/crates/relay-compiler/src/compiler.rs\nindex b7f8b1788471..71544d244b84 100644\n--- a/compiler/crates/relay-compiler/src/compiler.rs\n+++ b/compiler/crates/relay-compiler/src/compiler.rs\n@@ -268,6 +268,7 @@ pub fn build_raw_programs(\n     compiler_state: &CompilerState,\n     schemas: &FnvHashMap<ProjectName, Arc<SDLSchema>>,\n     log_event: &impl PerfLogEvent,\n+    compile_everything: bool,\n ) -> Result<(\n     ProgramMap,\n     AstMap,\n@@ -295,7 +296,8 @@ pub fn build_raw_programs(\n         })\n         .map(|project_config| {\n             let project_name = project_config.name;\n-            let is_incremental_build = compiler_state.has_processed_changes()\n+            let is_incremental_build = !compile_everything\n+                && compiler_state.has_processed_changes()\n                 && !compiler_state.has_breaking_schema_change(project_name)\n                 && if let Some(base) = project_config.base {\n                     !compiler_state.has_breaking_schema_change(base)\ndiff --git a/compiler/crates/relay-lsp/src/server/lsp_state_resources.rs b/compiler/crates/relay-lsp/src/server/lsp_state_resources.rs\nindex 7fb445194669..1d069d701078 100644\n--- a/compiler/crates/relay-lsp/src/server/lsp_state_resources.rs\n+++ b/compiler/crates/relay-lsp/src/server/lsp_state_resources.rs\n@@ -117,10 +117,9 @@ impl<TPerfLogger: PerfLogger + \'static> LSPStateResources<TPerfLogger> {\n             self.diagnostic_reporter.clear_regular_diagnostics();\n \n             // Run initial build, before entering the watch changes loop\n-            if let Err(err) = self.initial_build(&compiler_state, &setup_event) {\n+            if let Err(err) = self.initial_build(&mut compiler_state, &setup_event) {\n                 self.report_error(err);\n             }\n-            compiler_state.complete_compilation();\n \n             setup_event.stop(timer);\n             self.perf_logger.complete_event(setup_event);\n@@ -208,7 +207,7 @@ impl<TPerfLogger: PerfLogger + \'static> LSPStateResources<TPerfLogger> {\n \n     fn initial_build(\n         &self,\n-        compiler_state: &CompilerState,\n+        compiler_state: &mut CompilerState,\n         log_event: &impl PerfLogEvent,\n     ) -> Result<(), Error> {\n         debug!("Initial build started...");\n@@ -217,6 +216,7 @@ impl<TPerfLogger: PerfLogger + \'static> LSPStateResources<TPerfLogger> {\n         // if update detected\n         self.build_schemas(compiler_state, log_event)?;\n         self.build_source_programs(compiler_state, log_event)?;\n+        compiler_state.complete_compilation();\n \n         Ok(())\n     }\n@@ -286,6 +286,13 @@ impl<TPerfLogger: PerfLogger + \'static> LSPStateResources<TPerfLogger> {\n                 .read()\n                 .expect("LSPState::build_in_watch_mode: expect to acquire read lock on schemas"),\n             log_event,\n+            // When the source programs is empty, we need to compile all source programs once\n+            self.source_programs\n+                .read()\n+                .expect(\n+                    "LSPState::build_in_watch_mode: expect to acquire read lock on source_programs",\n+                )\n+                .is_empty(),\n         )?;\n \n         self.validate_programs(&programs, base_fragment_names_map, log_event)?;\n', 'commit_subject': 'Fix building full programs on initialization', 'repo_name': 'facebook/relay', 'committer': {'name': 'Facebook GitHub Bot', 'email': 'f08905f3496a7b7d60e0da97307f6ad7594abc92@users.noreply.github.com', 'time_sec': '1615405157', 'tz_offset': '-480', 'date': {'seconds': '1615405157'}}, 'commit_tree': 'b15187007b2c0726cb3e57492ded0d23d0284572', 'commit_message': 'Fix building full programs on initialization\n\nReviewed By: alunyov\n\nDifferential Revision: D26936595\n\nfbshipit-source-id: 735e045dd8f123ad50a46c966877aef629b92e87\n', 'language_name': 'Shell', 'license': 'mit', 'hunks': '@@ -117,10 +117,9 @@ impl<TPerfLogger: PerfLogger + \'static> LSPStateResources<TPerfLogger> {\n             self.diagnostic_reporter.clear_regular_diagnostics();\n \n             // Run initial build, before entering the watch changes loop\n-            if let Err(err) = self.initial_build(&compiler_state, &setup_event) {\n+            if let Err(err) = self.initial_build(&mut compiler_state, &setup_event) {\n                 self.report_error(err);\n             }\n-            compiler_state.complete_compilation();\n \n             setup_event.stop(timer);\n             self.perf_logger.complete_event(setup_event);\n@@ -208,7 +207,7 @@ impl<TPerfLogger: PerfLogger + \'static> LSPStateResources<TPerfLogger> {\n \n     fn initial_build(\n         &self,\n-        compiler_state: &CompilerState,\n+        compiler_state: &mut CompilerState,\n         log_event: &impl PerfLogEvent,\n     ) -> Result<(), Error> {\n         debug!("Initial build started...");\n@@ -217,6 +216,7 @@ impl<TPerfLogger: PerfLogger + \'static> LSPStateResources<TPerfLogger> {\n         // if update detected\n         self.build_schemas(compiler_state, log_event)?;\n         self.build_source_programs(compiler_state, log_event)?;\n+        compiler_state.complete_compilation();\n \n         Ok(())\n     }\n@@ -286,6 +286,13 @@ impl<TPerfLogger: PerfLogger + \'static> LSPStateResources<TPerfLogger> {\n                 .read()\n                 .expect("LSPState::build_in_watch_mode: expect to acquire read lock on schemas"),\n             log_event,\n+            // When the source programs is empty, we need to compile all source programs once\n+            self.source_programs\n+                .read()\n+                .expect(\n+                    "LSPState::build_in_watch_mode: expect to acquire read lock on source_programs",\n+                )\n+                .is_empty(),\n         )?;\n \n         self.validate_programs(&programs, base_fragment_names_map, log_event)?;\n', 'addition_count': 10, 'deletion_count': 3, 'src_file': 'a/compiler/crates/relay-lsp/src/server/lsp_state_resources.rs', 'tgt_file': 'b/compiler/crates/relay-lsp/src/server/lsp_state_resources.rs', 'file_extension': '.rs', 'before_file': '/*\n * Copyright (c) Meta Platforms, Inc. and affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nuse std::sync::Arc;\nuse std::sync::RwLock;\n\nuse common::PerfLogEvent;\nuse common::PerfLogger;\nuse dashmap::mapref::entry::Entry;\nuse fnv::FnvHashMap;\nuse graphql_ir::FragmentDefinitionNameSet;\nuse graphql_watchman::WatchmanFileSourceSubscriptionNextChange;\nuse intern::string_key::StringKey;\nuse log::debug;\nuse rayon::iter::ParallelIterator;\nuse relay_compiler::build_project::get_project_asts;\nuse relay_compiler::build_project::ProjectAstData;\nuse relay_compiler::build_project::ProjectAsts;\nuse relay_compiler::build_raw_program;\nuse relay_compiler::build_schema;\nuse relay_compiler::compiler_state::CompilerState;\nuse relay_compiler::compiler_state::ProjectName;\nuse relay_compiler::config::ProjectConfig;\nuse relay_compiler::errors::BuildProjectError;\nuse relay_compiler::errors::Error;\nuse relay_compiler::transform_program;\nuse relay_compiler::validate_program;\nuse relay_compiler::BuildProjectFailure;\nuse relay_compiler::FileSource;\nuse relay_compiler::FileSourceResult;\nuse relay_compiler::FileSourceSubscription;\nuse relay_compiler::FileSourceSubscriptionNextChange;\nuse relay_compiler::GraphQLAsts;\nuse relay_compiler::SourceControlUpdateStatus;\nuse schema::SDLSchema;\nuse schema_documentation::SchemaDocumentation;\nuse tokio::task;\nuse tokio::task::JoinHandle;\n\nuse super::lsp_state::ProjectStatus;\nuse super::lsp_state::Task;\nuse crate::status_updater::set_ready_status;\nuse crate::status_updater::update_in_progress_status;\nuse crate::LSPState;\n\n/// This structure is responsible for keeping schemas/programs in sync with the current state of the world\npub(crate) struct LSPStateResources<\n    TPerfLogger: PerfLogger + \'static,\n    TSchemaDocumentation: SchemaDocumentation + \'static,\n> {\n    lsp_state: Arc<LSPState<TPerfLogger, TSchemaDocumentation>>,\n}\n\nconst MAX_ERROR_COUNT: usize = 3;\n\nimpl<TPerfLogger: PerfLogger + \'static, TSchemaDocumentation: SchemaDocumentation + \'static>\n    LSPStateResources<TPerfLogger, TSchemaDocumentation>\n{\n    pub(crate) fn new(lsp_state: Arc<LSPState<TPerfLogger, TSchemaDocumentation>>) -> Self {\n        Self { lsp_state }\n    }\n\n    pub(crate) fn watch(self) {\n        tokio::spawn(async move {\n            // Wait for a notify from VSCode document events. This is for preventing\n            // starting the loop when the current workspace doesn\'t have any Relay file.\n            self.lsp_state.notify_lsp_state_resources.notified().await;\n            self.internal_watch().await;\n        });\n    }\n\n    /// Create an end-less loop of keeping the resources up-to-date with the source control changes\n    async fn internal_watch(&self) {\n        // avoid dead loop when watchman has an error\n        let mut error_count = 0;\n        \'outer: loop {\n            debug!("Initializing resources for LSP server");\n\n            update_in_progress_status(\n                "Relay: watchman...",\n                Some("Sending watchman query to get source files and possible saved state"),\n                &self.lsp_state.sender,\n            );\n            let setup_event = self\n                .lsp_state\n                .perf_logger\n                .create_event("lsp_state_initialize_resources");\n            let timer = setup_event.start("lsp_state_initialize_resources_time");\n\n            let file_source = match FileSource::connect(&self.lsp_state.config, &setup_event).await\n            {\n                Ok(f) => f,\n                Err(error) => {\n                    self.log_errors("watch_build_error", &error);\n                    error_count += 1;\n                    if error_count == MAX_ERROR_COUNT {\n                        panic!("{}", error);\n                    }\n                    continue;\n                }\n            };\n            let (mut compiler_state, file_source_subscription) = match file_source\n                .subscribe(&setup_event, self.lsp_state.perf_logger.as_ref())\n                .await\n            {\n                Ok(f) => f,\n                Err(error) => {\n                    self.log_errors("watch_build_error", &error);\n                    error_count += 1;\n                    if error_count == MAX_ERROR_COUNT {\n                        panic!("{}", error);\n                    }\n            self.diagnostic_reporter.clear_regular_diagnostics();\n\n            // Run initial build, before entering the watch changes loop\n            if let Err(err) = self.initial_build(&compiler_state, &setup_event) {\n                self.report_error(err);\n            }\n            compiler_state.complete_compilation();\n\n            setup_event.stop(timer);\n            self.perf_logger.complete_event(setup_event);\n            // If watchman detects the source control update, it will update the state\n            // of the source_control_update_status, so we can cancel building schemas/\n            // and programs earlier\n            let subscription_handle = self.watchman_subscription_handler(\n                file_source_subscription,\n                pending_file_source_changes,\n                Arc::clone(&compiler_state.source_control_update_status),\n            );\n            update_in_progress_status(\n                "Relay: creating state...",\n                Some("Building schemas and source programs for LSP"),\n                &self.lsp_state.sender,\n            );\n\n            self.lsp_state\n                .diagnostic_reporter\n                .clear_regular_diagnostics();\n\n            // Run initial build, before entering the watch changes loop\n            if let Err(error) = self.build_projects(&mut compiler_state, &setup_event) {\n                self.publish_errors(&error, "lsp_state_error");\n            }\n            set_ready_status(&self.lsp_state.sender);\n\n            setup_event.stop(timer);\n            setup_event.complete();\n\n            debug!("LSP server initialization completed!");\n            error_count = 0;\n\n            // Here we will wait for changes from watchman\n            \'inner: loop {\n                // Wait for a notify from watchman updates, or when a Relay file\n                // from an unactivated project is opened in VSCode\n                self.lsp_state.notify_lsp_state_resources.notified().await;\n\n                // Source control update started, we can ignore all pending changes, and wait for it to complete,\n                // we may change the status bar to `Source Control Update...`\n                if compiler_state.source_control_update_status.is_started() {\n                    update_in_progress_status(\n                        "Relay: hg update...",\n                        Some("Waiting for source control update"),\n                        &self.lsp_state.sender,\n                    );\n                    continue \'inner;\n                }\n\n                // SC Update completed, we need to abort current subscription, and re-initialize resource for LSP\n                if compiler_state.source_control_update_status.is_completed() {\n                    debug!("Watchman indicated the the source control update has completed!");\n                    subscription_handle.abort();\n                    continue \'outer;\n                }\n\n                let log_event = self\n                    .lsp_state\n                    .perf_logger\n                    .create_event("lsp_state_watchman_event");\n                let log_time = log_event.start("lsp_state_watchman_event_time");\n\n                if let Err(error) = self.incremental_build(&mut compiler_state, &log_event) {\n                    self.publish_errors(&error, "lsp_state_user_error");\n                }\n                set_ready_status(&self.lsp_state.sender);\n\n                log_event.stop(log_time);\n                log_event.complete();\n            }\n        }\n    }\n\n    fn incremental_build(\n        &self,\n        compiler_state: &mut CompilerState,\n        log_event: &impl PerfLogEvent,\n    ) -> Result<(), Error> {\n        let has_new_changes = compiler_state.merge_file_source_changes(\n            &self.lsp_state.config,\n            self.lsp_state.perf_logger.as_ref(),\n            false,\n        )?;\n\n    fn initial_build(\n        &self,\n        compiler_state: &CompilerState,\n        log_event: &impl PerfLogEvent,\n    ) -> Result<(), Error> {\n        debug!("Initial build started...");\n                .any(|r| r.value() == &ProjectStatus::Activated)\n        {\n        // if update detected\n        self.build_schemas(compiler_state, log_event)?;\n        self.build_source_programs(compiler_state, log_event)?;\n\n        Ok(())\n    }\n                "Relay: checking...",\n                Some("Validating changes, and updating source programs with the latest changes."),\n                &self.lsp_state.sender,\n            );\n\n            self.build_projects(compiler_state, log_event)?;\n        }\n\n        Ok(())\n    }\n\n    fn build_projects(\n        &self,\n        compiler_state: &mut CompilerState,\n        log_event: &impl PerfLogEvent,\n    ) -> Result<(), Error> {\n        let graphql_asts = log_event.time("parse_sources_time", || {\n            GraphQLAsts::from_graphql_sources_map(\n                &compiler_state.graphql_sources,\n                &compiler_state.get_dirty_definitions(&self.lsp_state.config),\n            )\n        })?;\n\n        if compiler_state.should_cancel_current_build() {\n            debug!("Build is cancelled: new file changes are pending.");\n            return Err(Error::Cancelled);\n        }\n\n        let timer = log_event.start("build_lsp_projects");\n        let build_results: Vec<_> = self\n            .lsp_state\n            .config\n            .par_enabled_projects()\n            .filter(|project_config| {\n                // Filter inactive projects\n                if !self\n                    .lsp_state\n                    .project_status\n                    .contains_key(&project_config.name)\n                {\n                    return false;\n                }\n                // When the source programs is empty, we need to compile all source programs once\n                if !self\n                    .lsp_state\n                    .source_programs\n                    .contains_key(&project_config.name)\n                {\n                    return true;\n                }\n                if let Some(base) = project_config.base {\n                    if compiler_state.project_has_pending_changes(base) {\n                        return true;\n                    }\n                }\n                compiler_state.project_has_pending_changes(project_config.name)\n            })\n            .map(|project_config| self.build_project(project_config, compiler_state, &graphql_asts))\n            .collect();\n        log_event.stop(timer);\n\n        let mut errors = vec![];\n        for build_result in build_results {\n                .read()\n                .expect("LSPState::build_in_watch_mode: expect to acquire read lock on schemas"),\n            log_event,\n        )?;\n\n        self.validate_programs(&programs, base_fragment_names_map, log_event)?;\n    }\n\n    fn build_project(\n        &self,\n        project_config: &ProjectConfig,\n        compiler_state: &CompilerState,\n        graphql_asts_map: &FnvHashMap<ProjectName, GraphQLAsts>,\n    ) -> Result<StringKey, BuildProjectFailure> {\n        self.lsp_state\n            .project_status\n            .insert(project_config.name, ProjectStatus::Completed);\n        let log_event = self.lsp_state.perf_logger.create_event("build_lsp_project");\n        let project_name = project_config.name;\n        let build_time = log_event.start("build_lsp_project_time");\n        log_event.string("project", project_name.to_string());\n\n        let schema = log_event.time("build_schema_time", || {\n            self.build_schema(compiler_state, project_config, graphql_asts_map)\n        })?;\n\n        let ProjectAstData {\n            project_asts,\n            base_fragment_names,\n        } = get_project_asts(&schema, graphql_asts_map, project_config)?;\n\n        // This will kick-off the validation for all synced sources\n        self.lsp_state.schedule_task(Task::ValidateSyncedSources);\n\n        self.build_programs(\n            project_config,\n            project_asts,\n            base_fragment_names,\n            compiler_state,\n            graphql_asts_map,\n            schema,\n            &log_event,\n        )?;\n\n        log_event.stop(build_time);\n        Ok(project_name)\n    }\n\n    fn build_schema(\n        &self,\n        compiler_state: &CompilerState,\n        project_config: &ProjectConfig,\n        graphql_asts_map: &FnvHashMap<ProjectName, GraphQLAsts>,\n    ) -> Result<Arc<SDLSchema>, BuildProjectFailure> {\n        match self.lsp_state.schemas.entry(project_config.name) {\n            Entry::Vacant(e) => {\n                let schema = build_schema(compiler_state, project_config, graphql_asts_map)\n                    .map_err(|errors| {\n                        BuildProjectFailure::Error(BuildProjectError::ValidationErrors {\n                            errors,\n                            project_name: project_config.name,\n                        })\n                    })?;\n                e.insert(Arc::clone(&schema));\n                Ok(schema)\n            }\n            Entry::Occupied(mut e) => {\n                if !compiler_state.project_has_pending_schema_changes(project_config.name) {\n                    Ok(Arc::clone(e.get()))\n                } else {\n                    let schema = build_schema(compiler_state, project_config, graphql_asts_map)\n                        .map_err(|errors| {\n                            debug!("build error");\n                            BuildProjectFailure::Error(BuildProjectError::ValidationErrors {\n                                errors,\n                                project_name: project_config.name,\n                            })\n                        })?;\n                    e.insert(Arc::clone(&schema));\n                    Ok(schema)\n                }\n            }\n        }\n    }\n\n    fn build_programs(\n        &self,\n        project_config: &ProjectConfig,\n        project_asts: ProjectAsts,\n        base_fragment_names: FragmentDefinitionNameSet,\n        compiler_state: &CompilerState,\n        graphql_asts: &FnvHashMap<ProjectName, GraphQLAsts>,\n        schema: Arc<SDLSchema>,\n        log_event: &impl PerfLogEvent,\n    ) -> Result<(), BuildProjectFailure> {\n        let is_incremental_build = self\n            .lsp_state\n            .source_programs\n            .contains_key(&project_config.name)\n            && compiler_state.has_processed_changes()\n            && !compiler_state\n                .has_breaking_schema_change(project_config.name, &project_config.schema_config)\n            && if let Some(base) = project_config.base {\n                !compiler_state.has_breaking_schema_change(base, &project_config.schema_config)\n            } else {\n                true\n            };\n\n        let (base_program, _) = build_raw_program(\n            project_config,\n            project_asts,\n            schema,\n            log_event,\n            is_incremental_build,\n        )?;\n\n        if compiler_state.should_cancel_current_build() {\n            debug!("Build is cancelled: updates in source code/or new file changes are pending.");\n            return Err(BuildProjectFailure::Cancelled);\n        }\n\n        match self.lsp_state.source_programs.entry(project_config.name) {\n            Entry::Vacant(e) => {\n                e.insert(base_program.clone());\n            }\n            Entry::Occupied(mut e) => {\n                let program = e.get_mut();\n                let removed_definition_names = graphql_asts\n                    .get(&project_config.name)\n                    .map(|ast| ast.removed_definition_names.as_ref());\n                program.merge_program(&base_program, removed_definition_names);\n            }\n        }\n\n        // Call validation rules that go beyond type checking.\n        validate_program(\n            &self.lsp_state.config,\n            project_config,\n            &base_program,\n            log_event,\n        )?;\n\n        transform_program(\n            project_config,\n            Arc::new(base_program),\n            Arc::new(base_fragment_names),\n            Arc::clone(&self.lsp_state.perf_logger),\n            log_event,\n            self.lsp_state.config.custom_transforms.as_ref(),\n        )?;\n        Ok(())\n    }\n\n    fn watchman_subscription_handler(\n        &self,\n        mut file_source_subscription: FileSourceSubscription,\n        pending_file_source_changes: Arc<RwLock<Vec<FileSourceResult>>>,\n        source_code_update_status: Arc<SourceControlUpdateStatus>,\n    ) -> JoinHandle<()> {\n        let notify_sender = self.lsp_state.notify_lsp_state_resources.clone();\n        task::spawn(async move {\n            loop {\n                match file_source_subscription.next_change().await {\n                    Err(_) => {\n                        // do nothing? compiler should panic, and restart the lsp\n                    }\n                    Ok(FileSourceSubscriptionNextChange::Watchman(watchman_next_change)) => {\n                        match watchman_next_change {\n                            WatchmanFileSourceSubscriptionNextChange::None => {}\n                            WatchmanFileSourceSubscriptionNextChange::SourceControlUpdateEnter => {\n                                source_code_update_status.mark_as_started();\n                            }\n                            WatchmanFileSourceSubscriptionNextChange::SourceControlUpdateLeave => {\n                                source_code_update_status.set_to_default();\n                            }\n                            WatchmanFileSourceSubscriptionNextChange::SourceControlUpdate => {\n                                source_code_update_status.mark_as_completed();\n                                notify_sender.notify_one();\n                                break;\n                            }\n                            WatchmanFileSourceSubscriptionNextChange::Result(\n                                file_source_changes,\n                            ) => {\n                                pending_file_source_changes\n                                .write()\n                                .expect("LSPState::watch_and_update_schemas: expect to acquire write lock on pending_file_source_changes")\n                                .push(FileSourceResult::Watchman(file_source_changes));\n\n                                notify_sender.notify_one();\n                            }\n                        }\n                    }\n                }\n            }\n        })\n    }\n\n    fn log_errors(&self, log_event_name: &\'static str, error: &Error) {\n        let error_event = self.lsp_state.perf_logger.create_event(log_event_name);\n        error_event.string("error", error.to_string());\n        error_event.complete();\n    }\n\n    /// Log errors and report the diagnostics to IDE\n    fn publish_errors(&self, error: &Error, log_event_name: &\'static str) {\n        self.lsp_state.diagnostic_reporter.report_error(error);\n        self.lsp_state.diagnostic_reporter.commit_diagnostics();\n        self.log_errors(log_event_name, error)\n    }\n}\n', 'message': 'Fix building full programs on initialization\n\nReviewed By: alunyov\n\nDifferential Revision: D26936595\n\nfbshipit-source-id: 735e045dd8f123ad50a46c966877aef629b92e87\n', 'author_name': 'Facebook GitHub Bot', 'lang': 'rs'}