Skip to main content

mago_fingerprint/
lib.rs

1use std::hash::BuildHasher;
2use std::hash::Hasher;
3
4use foldhash::fast::FixedState;
5
6use mago_names::ResolvedNames;
7
8pub mod access;
9pub mod argument;
10pub mod array;
11pub mod assignment;
12pub mod attribute;
13pub mod binary;
14pub mod block;
15pub mod call;
16pub mod class_like;
17pub mod clone;
18pub mod conditional;
19pub mod constant;
20pub mod construct;
21pub mod control_flow;
22pub mod declare;
23pub mod echo;
24pub mod expression;
25pub mod function_like;
26pub mod global;
27pub mod goto;
28pub mod halt_compiler;
29pub mod identifier;
30pub mod inline;
31pub mod instantiation;
32pub mod keyword;
33pub mod literal;
34pub mod r#loop;
35pub mod magic_constant;
36pub mod modifier;
37pub mod namespace;
38pub mod partial_application;
39pub mod pipe;
40pub mod program;
41pub mod r#return;
42pub mod statement;
43pub mod r#static;
44pub mod string;
45pub mod tag;
46pub mod terminator;
47pub mod throw;
48pub mod r#try;
49pub mod type_hint;
50pub mod unary;
51pub mod unset;
52pub mod r#use;
53pub mod variable;
54pub mod r#yield;
55
56const DEFAULT_IMPORTANT_COMMENT_PATTERNS: &[&str] = &["@mago-", "@"];
57
58pub trait Fingerprintable {
59    fn fingerprint(&self, resolved_names: &ResolvedNames, options: &FingerprintOptions<'_>) -> u64 {
60        let mut hasher = FixedState::default().build_hasher();
61        self.fingerprint_with_hasher(&mut hasher, resolved_names, options);
62        hasher.finish()
63    }
64
65    fn fingerprint_with_hasher<H: std::hash::Hasher>(
66        &self,
67        hasher: &mut H,
68        resolved_names: &ResolvedNames,
69        options: &FingerprintOptions<'_>,
70    );
71}
72
73impl<T: Fingerprintable> Fingerprintable for Option<T> {
74    fn fingerprint_with_hasher<H: std::hash::Hasher>(
75        &self,
76        hasher: &mut H,
77        resolved_names: &ResolvedNames,
78        options: &FingerprintOptions<'_>,
79    ) {
80        if let Some(value) = self {
81            value.fingerprint_with_hasher(hasher, resolved_names, options);
82        }
83    }
84}
85
86impl<T> Fingerprintable for &T
87where
88    T: Fingerprintable,
89{
90    fn fingerprint_with_hasher<H: std::hash::Hasher>(
91        &self,
92        hasher: &mut H,
93        resolved_names: &ResolvedNames,
94        options: &FingerprintOptions<'_>,
95    ) {
96        (*self).fingerprint_with_hasher(hasher, resolved_names, options);
97    }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct FingerprintOptions<'a> {
102    pub include_use_statements: bool,
103    pub important_comment_patterns: &'a [&'a str],
104    pub signature_only: bool,
105}
106
107impl Default for FingerprintOptions<'_> {
108    fn default() -> Self {
109        Self {
110            include_use_statements: false,
111            important_comment_patterns: DEFAULT_IMPORTANT_COMMENT_PATTERNS,
112            signature_only: false,
113        }
114    }
115}
116
117impl<'a> FingerprintOptions<'a> {
118    #[must_use]
119    pub fn new() -> Self {
120        Self::default()
121    }
122
123    #[must_use]
124    pub fn strict() -> Self {
125        Self { include_use_statements: true, important_comment_patterns: &[], signature_only: false }
126    }
127
128    #[must_use]
129    pub fn with_use_statements(mut self, include: bool) -> Self {
130        self.include_use_statements = include;
131        self
132    }
133
134    #[must_use]
135    pub fn with_comment_patterns(mut self, patterns: &'a [&'a str]) -> Self {
136        self.important_comment_patterns = patterns;
137        self
138    }
139
140    #[must_use]
141    pub fn is_important_comment(&self, comment: &str) -> bool {
142        for pattern in self.important_comment_patterns {
143            if comment.contains(pattern) {
144                return true;
145            }
146        }
147
148        false
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use bumpalo::Bump;
155    use indoc::indoc;
156    use mago_database::file::File;
157    use mago_names::resolver::NameResolver;
158    use mago_syntax::parser::parse_file;
159    use std::hash::Hasher;
160
161    use super::*;
162
163    pub(crate) fn fingerprint_code(code: &'static str) -> u64 {
164        let arena = Bump::new();
165        let file = File::ephemeral("code.php".into(), code.into());
166        let program = parse_file(&arena, &file);
167        assert!(!program.has_errors(), "Failed to parse code, errors: {:?}", program.errors);
168        let resolved_names = NameResolver::new(&arena).resolve(program);
169        let options = FingerprintOptions::default();
170
171        let mut hasher = foldhash::fast::FixedState::default().build_hasher();
172        program.fingerprint_with_hasher(&mut hasher, &resolved_names, &options);
173        hasher.finish()
174    }
175
176    #[test]
177    fn test_important_comment_detection() {
178        let opts = FingerprintOptions::default();
179
180        assert!(opts.is_important_comment("// @mago-ignore"));
181        assert!(opts.is_important_comment("/** @return string */"));
182        assert!(!opts.is_important_comment("// Regular comment"));
183        assert!(!opts.is_important_comment("/* Block comment */"));
184    }
185
186    #[test]
187    fn test_use_statement() {
188        let fp1 = fingerprint_code(indoc! {"
189            <?php
190
191            use Foo\\Bar;
192
193            $_ = new Bar();
194        "});
195
196        let fp2 = fingerprint_code(indoc! {"
197            <?php
198
199            $_ = new \\Foo\\Bar;
200        "});
201
202        let fp3 = fingerprint_code(indoc! {"
203            <?php
204
205            use Foo\\Bar; // Brrrr
206
207            $_ = new \\Foo\\Bar;
208        "});
209
210        let fp4 = fingerprint_code(indoc! {"
211            <?php
212
213            # Some comment
214            $_ = new Foo\\Bar();
215        "});
216
217        assert_eq!(fp1, fp2);
218        assert_eq!(fp1, fp3);
219        assert_eq!(fp1, fp4);
220    }
221
222    #[test]
223    fn test_docblock_comments_included() {
224        let code_with_doc = "<?php\n/** @return string */\nfunction foo() { return 'x'; }";
225        let code_without_doc = "<?php\nfunction foo() { return 'x'; }";
226
227        let fp1 = fingerprint_code(code_with_doc);
228        let fp2 = fingerprint_code(code_without_doc);
229
230        assert_ne!(fp1, fp2, "docblock comments with @ should change fingerprint");
231    }
232}