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}