mago_wasm/
analysis.rs

1//! # Analysis Module
2//!
3//! The `analysis` module contains the primary data structures and logic for
4//! performing a **combined parse, semantic analysis, lint, and (optional)**
5//! formatting pass on PHP code via Mago. It provides a high-level interface
6//! (see [`AnalysisResults::analyze`]) that accepts code plus configuration,
7//! and returns detailed information about the analyzed code, including parse
8//! and semantic issues, linter results, and a formatted output.
9
10use std::collections::HashSet;
11
12use serde::Serialize;
13
14use mago_formatter::Formatter;
15use mago_formatter::settings::FormatSettings;
16use mago_interner::StringIdentifier;
17use mago_interner::ThreadedInterner;
18use mago_linter::Linter;
19use mago_linter::settings::Settings;
20use mago_project::module::Module;
21use mago_project::module::ModuleBuildOptions;
22use mago_reporting::Issue;
23use mago_reporting::IssueCollection;
24use mago_source::Source;
25use mago_syntax::ast::Program;
26
27/// Represents the result of analyzing and formatting PHP code.
28///
29/// This struct encapsulates various aspects of the PHP code analysis process,
30/// providing detailed insights into the source code, including:
31///
32/// - Interned strings used in the code.
33/// - The resulting AST ([`Program`]) after parsing.
34/// - An optional parse error if the code was invalid.
35/// - Resolved name information (e.g., whether a name was imported).
36/// - A collection of semantic issues found during analysis.
37/// - A collection of linter issues found during linting (according to [`Settings`]).
38/// - Optionally, a `formatted` version of the code if no parse error was encountered.
39///
40/// **Note:** This struct is serialized into JSON (via [`serde`]) for use in
41/// WebAssembly and browser environments. For direct Rust usage, it can be
42/// used as-is in a native context.
43#[derive(Debug, Clone, Serialize)]
44pub struct AnalysisResults {
45    /// A set of interned strings used in the source code.
46    ///
47    /// Each string is represented as a tuple containing a [`StringIdentifier`]
48    /// and the actual string value.
49    pub strings: HashSet<(StringIdentifier, String)>,
50
51    /// The abstract syntax tree (AST) resulting from parsing the source code.
52    ///
53    /// If [`parse_error`](Self::parse_error) is `Some`, this AST may be incomplete or invalid.
54    pub program: Program,
55
56    /// An optional parse error, if one occurred during parsing.
57    ///
58    /// If this is `Some`, then [`formatted`](Self::formatted) will be `None`,
59    /// because the code could not be validly parsed.
60    pub parse_error: Option<Issue>,
61
62    /// The resolved names within the source code.
63    ///
64    /// Each entry is a tuple `(byte_offset, (identifier, imported))`, where:
65    /// - `byte_offset` indicates where in the source the name is used,
66    /// - `identifier` is the [`StringIdentifier`] for the interned name,
67    /// - `imported` indicates whether this name was imported or locally declared.
68    pub names: HashSet<(usize, (StringIdentifier, bool))>,
69
70    /// The formatted version of the source code, if there were no parse errors.
71    ///
72    /// This is produced by Mago’s internal formatter and is only set if
73    /// [`parse_error`](Self::parse_error) is `None`.
74    pub formatted: Option<String>,
75
76    /// A collection of semantic issues found during semantic analysis.
77    ///
78    /// These might include invalid modifier usage, incorrect return types,
79    /// or undefined variable references, among others.
80    pub semantic_issues: IssueCollection,
81
82    /// A collection of linter issues discovered based on the specified
83    /// [`Settings`](mago_linter::settings::Settings).
84    ///
85    /// Includes warnings or notices about potential coding standard violations,
86    /// unused variables, or other recommended improvements.
87    pub linter_issues: IssueCollection,
88}
89
90impl AnalysisResults {
91    /// Analyzes and (optionally) formats the provided PHP `code`.
92    ///
93    /// This function performs the following steps:
94    /// 1. **Parsing** via [`Module::build`] to generate an AST and detect syntax errors.
95    /// 2. **Semantic Analysis** to resolve names and check for issues (e.g. undefined variables).
96    /// 3. **Linting** according to the specified [`Settings`](mago_linter::settings::Settings).
97    /// 4. **Formatting** using [`FormatSettings`](mago_formatter::settings::FormatSettings),
98    ///    provided no parse errors were found.
99    ///
100    /// # Arguments
101    ///
102    /// * `code` - A `String` containing the PHP code to be analyzed.
103    /// * `linter_settings` - Configuration for which linter plugins and rules should run.
104    /// * `format_settings` - Configuration for how the code should be formatted.
105    ///
106    /// # Returns
107    ///
108    /// Returns an [`AnalysisResults`] containing the AST, parse/semantic/linter issues,
109    /// and formatted code (if no parse error).
110    pub fn analyze(code: String, lint_settings: Settings, format_settings: FormatSettings) -> Self {
111        let interner = ThreadedInterner::new();
112        let source = Source::standalone(&interner, "code.php", &code);
113        let (mut module, program) =
114            Module::build_with_ast(&interner, lint_settings.php_version, source, ModuleBuildOptions::validation());
115        let mut formatted = None;
116        if module.parse_error.is_none() {
117            let formatter = Formatter::new(&interner, lint_settings.php_version, format_settings);
118
119            // Only format if there are no parse errors
120            formatted = Some(formatter.format(&module.source, &program));
121        }
122
123        let linter =
124            Linter::with_all_plugins(lint_settings, interner.clone(), module.reflection.take().unwrap_or_default());
125        let linter_issues = linter.lint(&module);
126
127        Self {
128            strings: interner.all().into_iter().map(|(id, value)| (id, value.to_string())).collect(),
129            program,
130            parse_error: module.parse_error.as_ref().map(|e| e.into()),
131            names: module.names.all().into_iter().map(|(offset, (id, imported))| (*offset, (*id, *imported))).collect(),
132            formatted,
133            semantic_issues: module.issues,
134            linter_issues,
135        }
136    }
137}