Run PHPStan in editor mode on the given buffer content and return LSP diagnostics. `file_path` is the real path of the file on disk (used for the `--instead-of` flag). `content` is the current editor buffer (which may differ from the on-disk version). `workspace_root` is needed to run PHPStan from the project root directory so that it picks up `phpstan.neon` / `phpstan.neon.dist`.
(
resolved: &ResolvedPhpStan,
content: &str,
file_path: &Path,
workspace_root: &Path,
config: &PhpStanConfig,
cancelled: &std::sync::atomic::AtomicBool,
)
| 143 | /// `workspace_root` is needed to run PHPStan from the project root |
| 144 | /// directory so that it picks up `phpstan.neon` / `phpstan.neon.dist`. |
| 145 | pub(crate) fn run_phpstan( |
| 146 | resolved: &ResolvedPhpStan, |
| 147 | content: &str, |
| 148 | file_path: &Path, |
| 149 | workspace_root: &Path, |
| 150 | config: &PhpStanConfig, |
| 151 | cancelled: &std::sync::atomic::AtomicBool, |
| 152 | ) -> Result<Vec<Diagnostic>, String> { |
| 153 | let timeout_ms = config.timeout.unwrap_or(DEFAULT_TIMEOUT_MS); |
| 154 | let timeout = Duration::from_millis(timeout_ms); |
| 155 | let memory_limit = config.memory_limit.as_deref().unwrap_or("1G"); |
| 156 | |
| 157 | // Write the buffer to a temp file. We use the system temp dir |
| 158 | // (not a sibling file) because PHPStan's --tmp-file is designed |
| 159 | // to work with arbitrary temp paths, and we avoid polluting the |
| 160 | // project directory. |
| 161 | let tmp = write_temp_file(file_path, content)?; |
| 162 | let tmp_path = tmp.path().to_path_buf(); |
| 163 | |
| 164 | // Build the PHPStan command. |
| 165 | // |
| 166 | // The file path is passed as a positional argument so that PHPStan |
| 167 | // only analyses this single file, not the entire project. Without |
| 168 | // it, PHPStan would analyse all paths from phpstan.neon, which can |
| 169 | // take minutes on large codebases. The `--tmp-file` / `--instead-of` |
| 170 | // flags tell PHPStan to substitute the file's content but do NOT |
| 171 | // limit the analysis scope. |
| 172 | let mut cmd = Command::new(&resolved.path); |
| 173 | cmd.arg("analyse") |
| 174 | .arg("--error-format=json") |
| 175 | .arg("--no-progress") |
| 176 | .arg("--no-ansi") |
| 177 | .arg(format!("--memory-limit={}", memory_limit)) |
| 178 | .arg(format!("--tmp-file={}", tmp_path.display())) |
| 179 | .arg(format!("--instead-of={}", file_path.display())) |
| 180 | .arg(file_path) |
| 181 | .current_dir(workspace_root); |
| 182 | |
| 183 | let result = run_command_with_timeout(&mut cmd, timeout, cancelled); |
| 184 | |
| 185 | // NamedTempFile auto-deletes on drop — but we keep `tmp` alive |
| 186 | // until after we've consumed the command output. |
| 187 | let _ = &tmp; |
| 188 | |
| 189 | match result { |
| 190 | Ok(output) => { |
| 191 | // PHPStan exit codes: |
| 192 | // 0 = no errors found |
| 193 | // 1 = errors found (this is the normal "has diagnostics" case) |
| 194 | // 2+ = internal error / misconfiguration |
| 195 | match output.code { |
| 196 | 0 => Ok(Vec::new()), |
| 197 | 1 => parse_phpstan_json(&output.stdout, file_path), |
| 198 | _ => { |
| 199 | // For exit code 2+, check if there's still usable JSON |
| 200 | // output (PHPStan sometimes returns code 2 with partial |
| 201 | // results). If parsing fails, report the error. |
| 202 | match parse_phpstan_json(&output.stdout, file_path) { |
no test coverage detected