@@ -148,7 +148,6 @@ pub fn get_doc(
148148 attrs : & mut Vec < syn:: Attribute > ,
149149 mut text_signature : Option < String > ,
150150 ctx : & Ctx ,
151- ) -> syn:: Result < PythonDoc > {
152151) -> syn:: Result < PythonDoc > {
153152 let Ctx { pyo3_path, .. } = ctx;
154153 // insert special divider between `__text_signature__` and doc
@@ -160,6 +159,7 @@ pub fn get_doc(
160159 let mut parts = Punctuated :: < TokenStream , Token ! [ , ] > :: new ( ) ;
161160 let mut first = true ;
162161 let mut current_part = text_signature. unwrap_or_default ( ) ;
162+ let mut current_part_span = None ; // Track span for error reporting
163163
164164 let mut mode = DocParseMode :: Both ;
165165
@@ -176,6 +176,11 @@ pub fn get_doc(
176176 ..
177177 } ) = & nv. value
178178 {
179+ // Update span for error reporting
180+ if current_part_span. is_none ( ) {
181+ current_part_span = Some ( lit_str. span ( ) ) ;
182+ }
183+
179184 // Strip single left space from literal strings, if needed.
180185 let doc_line = lit_str. value ( ) ;
181186 let stripped_line = doc_line. strip_prefix ( ' ' ) . unwrap_or ( & doc_line) ;
@@ -190,11 +195,25 @@ pub fn get_doc(
190195 "python" => DocParseMode :: PythonOnly ,
191196 "rust" => DocParseMode :: RustOnly ,
192197 "both" => DocParseMode :: Both ,
193- _ => return Err ( syn:: Error :: new ( lit_str. span ( ) , format ! ( "Invalid doc_mode: '{}'. Expected 'python', 'rust', or 'both'." , value) ) ) ,
198+ _ => return Err ( syn:: Error :: new (
199+ lit_str. span ( ) ,
200+ format ! ( "Invalid doc_mode: '{}'. Expected 'python', 'rust', or 'both'." , value)
201+ ) ) ,
194202 } ;
195203 // Do not retain mode switch lines in Rust, and skip in Python
196204 continue ;
205+ } else if is_likely_pyo3_doc_mode_typo ( content_trimmed) {
206+ // Handle potential typos in pyo3_doc_mode prefix
207+ return Err ( syn:: Error :: new (
208+ lit_str. span ( ) ,
209+ format ! (
210+ "Suspicious comment '{}' - did you mean 'pyo3_doc_mode'? Valid format: <!-- pyo3_doc_mode: python/rust/both -->" ,
211+ content_trimmed
212+ )
213+ ) ) ;
197214 }
215+ // If it's an HTML comment but not pyo3_doc_mode related,
216+ // it will be included based on current mode (no special handling)
198217 }
199218
200219 // Not a mode switch, decide based on current mode
@@ -219,8 +238,10 @@ pub fn get_doc(
219238 // Include in Python doc if needed
220239 if include_in_python {
221240 // Reset the string buffer, write that part, and then push this macro part too.
222- parts. push ( current_part. to_token_stream ( ) ) ;
223- current_part. clear ( ) ;
241+ if !current_part. is_empty ( ) {
242+ parts. push ( current_part. to_token_stream ( ) ) ;
243+ current_part. clear ( ) ;
244+ }
224245 parts. push ( nv. value . to_token_stream ( ) ) ;
225246 }
226247 }
@@ -241,7 +262,10 @@ pub fn get_doc(
241262
242263 // Check if mode ended in Both; if not, error to enforce "pairing"
243264 if !matches ! ( mode, DocParseMode :: Both ) {
244- return Err ( err_spanned ! ( Span :: call_site( ) => "doc_mode did not end in 'both' mode; consider adding <!-- pyo3_doc_mode: both --> at the end" ) ) ;
265+ return Err ( syn:: Error :: new (
266+ Span :: call_site ( ) ,
267+ "doc_mode did not end in 'both' mode; consider adding <!-- pyo3_doc_mode: both --> at the end"
268+ ) ) ;
245269 }
246270
247271 if !parts. is_empty ( ) {
@@ -256,16 +280,13 @@ pub fn get_doc(
256280
257281 syn:: Ident :: new ( "concat" , Span :: call_site ( ) ) . to_tokens ( & mut tokens) ;
258282 syn:: token:: Not ( Span :: call_site ( ) ) . to_tokens ( & mut tokens) ;
259- syn:: token:: Bracket ( Span :: call_site ( ) ) . surround ( & mut tokens, |tokens| {
283+ syn:: token:: Paren ( Span :: call_site ( ) ) . surround ( & mut tokens, |tokens| {
260284 parts. to_tokens ( tokens) ;
261- syn:: token:: Comma ( Span :: call_site ( ) ) . to_tokens ( tokens) ;
262285 } ) ;
263286
264- Ok ( PythonDoc ( PythonDocKind :: Tokens (
265287 Ok ( PythonDoc ( PythonDocKind :: Tokens (
266288 quote ! ( #pyo3_path:: ffi:: c_str!( #tokens) ) ,
267289 ) ) )
268- ) ) )
269290 } else {
270291 // Just a string doc - return directly with nul terminator
271292 let docs = CString :: new ( current_part) . unwrap ( ) ;
@@ -274,10 +295,63 @@ pub fn get_doc(
274295 current_part_span. unwrap_or ( Span :: call_site ( ) ) ,
275296 ctx,
276297 ) ) ) )
277- ) ) ) )
278298 }
279299}
280300
301+ /// Helper function to detect likely typos in pyo3_doc_mode prefix
302+ fn is_likely_pyo3_doc_mode_typo ( content : & str ) -> bool {
303+ // Simple fuzzy matching for common typos
304+ let potential_typos = [
305+ "pyo3_doc_mde" ,
306+ "pyo3_docc_mode" ,
307+ "pyo3_doc_mod" ,
308+ "py03_doc_mode" ,
309+ "pyo3doc_mode" ,
310+ "pyo3_docmode" ,
311+ "pyo_doc_mode" ,
312+ "pyo3_doc_node" ,
313+ ] ;
314+
315+ potential_typos. iter ( ) . any ( |& typo| {
316+ content. starts_with ( typo) ||
317+ ( content. len ( ) >= typo. len ( ) - 2 &&
318+ simple_edit_distance ( content. split ( ':' ) . next ( ) . unwrap_or ( "" ) , typo) <= 2 )
319+ } )
320+ }
321+
322+ /// Simple edit distance calculation for typo detection
323+ fn simple_edit_distance ( a : & str , b : & str ) -> usize {
324+ let a_chars: Vec < char > = a. chars ( ) . collect ( ) ;
325+ let b_chars: Vec < char > = b. chars ( ) . collect ( ) ;
326+ let a_len = a_chars. len ( ) ;
327+ let b_len = b_chars. len ( ) ;
328+
329+ if a_len == 0 { return b_len; }
330+ if b_len == 0 { return a_len; }
331+
332+ let mut matrix = vec ! [ vec![ 0 ; b_len + 1 ] ; a_len + 1 ] ;
333+
334+ // Initialize first row and column
335+ for i in 0 ..=a_len {
336+ matrix[ i] [ 0 ] = i;
337+ }
338+ for j in 0 ..=b_len {
339+ matrix[ 0 ] [ j] = j;
340+ }
341+
342+ // Fill the matrix
343+ for i in 1 ..=a_len {
344+ for j in 1 ..=b_len {
345+ let cost = if a_chars[ i - 1 ] == b_chars[ j - 1 ] { 0 } else { 1 } ;
346+ matrix[ i] [ j] = ( matrix[ i - 1 ] [ j] + 1 ) // deletion
347+ . min ( matrix[ i] [ j - 1 ] + 1 ) // insertion
348+ . min ( matrix[ i - 1 ] [ j - 1 ] + cost) ; // substitution
349+ }
350+ }
351+
352+ matrix[ a_len] [ b_len]
353+ }
354+
281355impl quote:: ToTokens for PythonDoc {
282356 fn to_tokens ( & self , tokens : & mut TokenStream ) {
283357 match & self . 0 {
0 commit comments