1#![warn(missing_docs)]
39
40mod kind;
41mod token;
42
43pub use kind::{
44 DELIMITER_SPELLINGS, KEYWORD_SPELLINGS, OPERATOR_SPELLINGS, SIGIL_SPELLINGS, TokenCategory,
45 TokenKind, TokenKindMetadata,
46};
47pub use token::{Token, TokenRef, TokenSpan, TokenSpanError};
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
56 fn token_span_new_and_accessors() {
57 let span = TokenSpan::new(5, 10);
58 assert_eq!(span.start, 5);
59 assert_eq!(span.end, 10);
60 assert_eq!(span.len(), 5);
61 assert!(!span.is_empty());
62 assert_eq!(span.range(), 5..10);
63 }
64
65 #[test]
66 fn token_span_is_empty_when_zero_length() {
67 let span = TokenSpan::new(3, 3);
68 assert!(span.is_empty());
69 assert_eq!(span.len(), 0);
70 }
71
72 #[test]
73 fn token_span_try_new_ok() -> Result<(), TokenSpanError> {
74 let span = TokenSpan::try_new(0, 5)?;
75 assert_eq!(span.start, 0);
76 assert_eq!(span.end, 5);
77 Ok(())
78 }
79
80 #[test]
81 fn token_span_try_new_end_before_start_errors() {
82 assert_eq!(
83 TokenSpan::try_new(10, 5),
84 Err(TokenSpanError::EndBeforeStart { start: 10, end: 5 })
85 );
86 }
87
88 #[test]
89 fn token_span_error_display_end_before_start() {
90 let err = TokenSpanError::EndBeforeStart { start: 10, end: 5 };
91 let msg = err.to_string();
92 assert!(msg.contains("10"));
93 assert!(msg.contains("5"));
94 }
95
96 #[test]
97 fn token_span_error_display_empty_span_not_allowed() {
98 let err = TokenSpanError::EmptySpanNotAllowed { kind: TokenKind::Identifier, at: 7 };
99 let msg = err.to_string();
100 assert!(msg.contains("Identifier"));
101 assert!(msg.contains("7"));
102 }
103
104 #[test]
107 fn token_new_stores_fields() {
108 let tok = Token::new(TokenKind::My, "my", 0, 2);
109 assert_eq!(tok.kind, TokenKind::My);
110 assert_eq!(&*tok.text, "my");
111 assert_eq!(tok.start, 0);
112 assert_eq!(tok.end, 2);
113 }
114
115 #[test]
116 fn token_len_saturates_for_inverted_span() {
117 let tok = Token::new(TokenKind::Identifier, "x", 9, 4);
118 assert_eq!(tok.len(), 0);
119 assert!(tok.is_empty());
120 }
121
122 #[test]
123 fn token_len_and_is_empty() {
124 let tok = Token::new(TokenKind::Identifier, "foo", 10, 13);
125 assert_eq!(tok.len(), 3);
126 assert!(!tok.is_empty());
127
128 let eof = Token::eof_at(8);
129 assert_eq!(eof.len(), 0);
130 assert!(eof.is_empty());
131 }
132
133 #[test]
134 fn token_span_and_range() {
135 let tok = Token::new(TokenKind::Number, "42", 5, 7);
136 assert_eq!(tok.span(), TokenSpan::new(5, 7));
137 assert_eq!(tok.range(), 5..7);
138 }
139
140 #[test]
141 fn token_try_new_allows_ordered_spans() -> Result<(), TokenSpanError> {
142 let tok = Token::try_new(TokenKind::Identifier, "name", 4, 8)?;
143 assert_eq!(tok.kind, TokenKind::Identifier);
144 assert_eq!(&*tok.text, "name");
145 assert_eq!(tok.span(), TokenSpan::new(4, 8));
146 Ok(())
147 }
148
149 #[test]
150 fn token_try_new_rejects_end_before_start() {
151 assert_eq!(
152 Token::try_new(TokenKind::Identifier, "x", 10, 5),
153 Err(TokenSpanError::EndBeforeStart { start: 10, end: 5 })
154 );
155 }
156
157 #[test]
158 fn token_new_checked_rejects_empty_non_eof() {
159 assert_eq!(
160 Token::new_checked(TokenKind::Identifier, "", 5, 5),
161 Err(TokenSpanError::EmptySpanNotAllowed { kind: TokenKind::Identifier, at: 5 })
162 );
163 }
164
165 #[test]
166 fn token_new_checked_allows_empty_eof() -> Result<(), TokenSpanError> {
167 let tok = Token::new_checked(TokenKind::Eof, "", 5, 5)?;
168 assert_eq!(tok.kind, TokenKind::Eof);
169 assert_eq!(tok.start, 5);
170 Ok(())
171 }
172
173 #[test]
174 fn token_new_checked_allows_empty_unknown() -> Result<(), TokenSpanError> {
175 let tok = Token::new_checked(TokenKind::Unknown, "", 6, 6)?;
176 assert_eq!(tok.kind, TokenKind::Unknown);
177 assert_eq!(tok.start, 6);
178 assert!(tok.is_empty());
179 Ok(())
180 }
181
182 #[test]
183 fn token_eof_at() {
184 let eof = Token::eof_at(42);
185 assert_eq!(eof.kind, TokenKind::Eof);
186 assert_eq!(eof.start, 42);
187 assert_eq!(eof.end, 42);
188 assert!(eof.is_empty());
189 }
190
191 #[test]
192 fn token_unknown_at_normalises_inverted_span() {
193 let tok = Token::unknown_at("?", 5, 3); assert_eq!(tok.kind, TokenKind::Unknown);
195 assert_eq!(tok.start, 5);
196 assert_eq!(tok.end, 5); }
198
199 #[test]
200 fn token_with_kind() {
201 let tok = Token::new(TokenKind::Identifier, "sub", 0, 3);
202 let retyped = tok.with_kind(TokenKind::Sub);
203 assert_eq!(retyped.kind, TokenKind::Sub);
204 assert_eq!(&*retyped.text, "sub");
205 assert_eq!(retyped.start, 0);
206 assert_eq!(retyped.end, 3);
207 }
208
209 #[test]
210 fn token_with_span_ok() -> Result<(), TokenSpanError> {
211 let tok = Token::new(TokenKind::String, "hello", 0, 5);
212 let moved = tok.with_span(10, 15)?;
213 assert_eq!(moved.start, 10);
214 assert_eq!(moved.end, 15);
215 Ok(())
216 }
217
218 #[test]
219 fn token_with_span_rejects_empty_non_eof() {
220 let tok = Token::new(TokenKind::String, "hello", 0, 5);
221 assert_eq!(
222 tok.with_span(10, 10),
223 Err(TokenSpanError::EmptySpanNotAllowed { kind: TokenKind::String, at: 10 })
224 );
225 }
226
227 #[test]
228 fn token_display_name_delegates_to_kind() {
229 let tok = Token::new(TokenKind::LeftBrace, "{", 0, 1);
230 assert_eq!(tok.display_name(), "'{'");
231 }
232
233 #[test]
234 fn token_as_ref_token_round_trip() {
235 let tok = Token::new(TokenKind::Sub, "sub", 0, 3);
236 let tok_ref = tok.as_ref_token();
237 assert_eq!(tok_ref.kind, TokenKind::Sub);
238 assert_eq!(tok_ref.text, "sub");
239 assert_eq!(tok_ref.start, 0);
240 assert_eq!(tok_ref.end, 3);
241
242 let owned: Token = tok_ref.into();
243 assert_eq!(owned.kind, TokenKind::Sub);
244 assert_eq!(&*owned.text, "sub");
245 }
246
247 #[test]
250 fn token_ref_len_saturates_for_inverted_span() {
251 let r = TokenRef::new(TokenKind::Identifier, "x", 9, 4);
252 assert_eq!(r.len(), 0);
253 assert!(r.is_empty());
254 }
255
256 #[test]
257 fn token_ref_accessors() {
258 let r = TokenRef::new(TokenKind::Number, "99", 4, 6);
259 assert_eq!(r.len(), 2);
260 assert!(!r.is_empty());
261 assert_eq!(r.span(), (4, 6));
262 assert_eq!(r.display_name(), "number");
263 }
264
265 #[test]
266 fn token_ref_try_new_allows_ordered_spans() -> Result<(), TokenSpanError> {
267 let r = TokenRef::try_new(TokenKind::Number, "99", 4, 6)?;
268 assert_eq!(r.kind, TokenKind::Number);
269 assert_eq!(r.text, "99");
270 assert_eq!(r.span(), (4, 6));
271 Ok(())
272 }
273
274 #[test]
275 fn token_ref_to_owned_token() {
276 let r = TokenRef::new(TokenKind::Identifier, "foo", 1, 4);
277 let owned = r.to_owned_token();
278 assert_eq!(owned.kind, TokenKind::Identifier);
279 assert_eq!(&*owned.text, "foo");
280 }
281
282 #[test]
285 fn from_keyword_recognises_perl_keywords() {
286 assert_eq!(TokenKind::from_keyword("my"), Some(TokenKind::My));
287 assert_eq!(TokenKind::from_keyword("sub"), Some(TokenKind::Sub));
288 assert_eq!(TokenKind::from_keyword("if"), Some(TokenKind::If));
289 assert_eq!(TokenKind::from_keyword("elsif"), Some(TokenKind::Elsif));
290 assert_eq!(TokenKind::from_keyword("else"), Some(TokenKind::Else));
291 assert_eq!(TokenKind::from_keyword("while"), Some(TokenKind::While));
292 assert_eq!(TokenKind::from_keyword("for"), Some(TokenKind::For));
293 assert_eq!(TokenKind::from_keyword("foreach"), Some(TokenKind::Foreach));
294 assert_eq!(TokenKind::from_keyword("return"), Some(TokenKind::Return));
295 assert_eq!(TokenKind::from_keyword("package"), Some(TokenKind::Package));
296 assert_eq!(TokenKind::from_keyword("use"), Some(TokenKind::Use));
297 assert_eq!(TokenKind::from_keyword("BEGIN"), Some(TokenKind::Begin));
298 assert_eq!(TokenKind::from_keyword("END"), Some(TokenKind::End));
299 assert_eq!(TokenKind::from_keyword("eval"), Some(TokenKind::Eval));
300 assert_eq!(TokenKind::from_keyword("class"), Some(TokenKind::Class));
301 assert_eq!(TokenKind::from_keyword("defer"), Some(TokenKind::Defer));
302 assert_eq!(TokenKind::from_keyword("and"), Some(TokenKind::WordAnd));
303 assert_eq!(TokenKind::from_keyword("or"), Some(TokenKind::WordOr));
304 assert_eq!(TokenKind::from_keyword("not"), Some(TokenKind::WordNot));
305 assert_eq!(TokenKind::from_keyword("xor"), Some(TokenKind::WordXor));
306 assert_eq!(TokenKind::from_keyword("cmp"), Some(TokenKind::StringCompare));
307 }
308
309 #[test]
310 fn from_keyword_unknown_returns_none() {
311 assert_eq!(TokenKind::from_keyword("MY"), None);
312 assert_eq!(TokenKind::from_keyword("Sub"), None);
313 assert_eq!(TokenKind::from_keyword("unknown"), None);
314 assert_eq!(TokenKind::from_keyword(""), None);
315 }
316
317 #[test]
320 fn from_operator_recognises_operators() {
321 assert_eq!(TokenKind::from_operator("="), Some(TokenKind::Assign));
322 assert_eq!(TokenKind::from_operator("+"), Some(TokenKind::Plus));
323 assert_eq!(TokenKind::from_operator("**"), Some(TokenKind::Power));
324 assert_eq!(TokenKind::from_operator("->"), Some(TokenKind::Arrow));
325 assert_eq!(TokenKind::from_operator("=>"), Some(TokenKind::FatArrow));
326 assert_eq!(TokenKind::from_operator("<=>"), Some(TokenKind::Spaceship));
327 assert_eq!(TokenKind::from_operator("//="), Some(TokenKind::DefinedOrAssign));
328 assert_eq!(TokenKind::from_operator("..."), Some(TokenKind::Ellipsis));
329 assert_eq!(TokenKind::from_operator("~~"), Some(TokenKind::SmartMatch));
330 }
331
332 #[test]
333 fn from_operator_unknown_returns_none() {
334 assert_eq!(TokenKind::from_operator(""), None);
335 assert_eq!(TokenKind::from_operator("xyz"), None);
336 }
337
338 #[test]
341 fn from_delimiter_recognises_all() {
342 assert_eq!(TokenKind::from_delimiter("("), Some(TokenKind::LeftParen));
343 assert_eq!(TokenKind::from_delimiter(")"), Some(TokenKind::RightParen));
344 assert_eq!(TokenKind::from_delimiter("{"), Some(TokenKind::LeftBrace));
345 assert_eq!(TokenKind::from_delimiter("}"), Some(TokenKind::RightBrace));
346 assert_eq!(TokenKind::from_delimiter("["), Some(TokenKind::LeftBracket));
347 assert_eq!(TokenKind::from_delimiter("]"), Some(TokenKind::RightBracket));
348 assert_eq!(TokenKind::from_delimiter(";"), Some(TokenKind::Semicolon));
349 assert_eq!(TokenKind::from_delimiter(","), Some(TokenKind::Comma));
350 assert_eq!(TokenKind::from_delimiter("x"), None);
351 }
352
353 #[test]
356 fn from_sigil_recognises_all() {
357 assert_eq!(TokenKind::from_sigil("$"), Some(TokenKind::ScalarSigil));
358 assert_eq!(TokenKind::from_sigil("@"), Some(TokenKind::ArraySigil));
359 assert_eq!(TokenKind::from_sigil("%"), Some(TokenKind::HashSigil));
360 assert_eq!(TokenKind::from_sigil("&"), Some(TokenKind::SubSigil));
361 assert_eq!(TokenKind::from_sigil("*"), Some(TokenKind::GlobSigil));
362 assert_eq!(TokenKind::from_sigil("!"), None);
363 }
364
365 #[test]
368 fn category_keyword_variants() {
369 assert_eq!(TokenKind::My.category(), TokenCategory::Keyword);
370 assert_eq!(TokenKind::Sub.category(), TokenCategory::Keyword);
371 assert_eq!(TokenKind::Defer.category(), TokenCategory::Keyword);
372 }
373
374 #[test]
375 fn category_operator_variants() {
376 assert_eq!(TokenKind::Plus.category(), TokenCategory::Operator);
377 assert_eq!(TokenKind::Spaceship.category(), TokenCategory::Operator);
378 assert_eq!(TokenKind::WordAnd.category(), TokenCategory::Operator);
379 }
380
381 #[test]
382 fn category_delimiter_variants() {
383 assert_eq!(TokenKind::LeftParen.category(), TokenCategory::Delimiter);
384 assert_eq!(TokenKind::Comma.category(), TokenCategory::Delimiter);
385 }
386
387 #[test]
388 fn category_literal_variants() {
389 assert_eq!(TokenKind::Number.category(), TokenCategory::Literal);
390 assert_eq!(TokenKind::HeredocStart.category(), TokenCategory::Literal);
391 assert_eq!(TokenKind::DataMarker.category(), TokenCategory::Literal);
392 }
393
394 #[test]
395 fn category_identifier_variants() {
396 assert_eq!(TokenKind::Identifier.category(), TokenCategory::Identifier);
397 assert_eq!(TokenKind::ScalarSigil.category(), TokenCategory::Identifier);
398 assert_eq!(TokenKind::GlobSigil.category(), TokenCategory::Identifier);
399 }
400
401 #[test]
402 fn category_special_variants() {
403 assert_eq!(TokenKind::Eof.category(), TokenCategory::Special);
404 assert_eq!(TokenKind::Unknown.category(), TokenCategory::Special);
405 }
406
407 #[test]
410 fn display_name_selected_variants() {
411 assert_eq!(TokenKind::LeftBrace.display_name(), "'{'");
412 assert_eq!(TokenKind::RightBrace.display_name(), "'}'");
413 assert_eq!(TokenKind::Identifier.display_name(), "identifier");
414 assert_eq!(TokenKind::Eof.display_name(), "end of input");
415 assert_eq!(TokenKind::Number.display_name(), "number");
416 assert_eq!(TokenKind::Sub.display_name(), "'sub'");
417 assert_eq!(TokenKind::Semicolon.display_name(), "';'");
418 assert_eq!(TokenKind::HeredocStart.display_name(), "heredoc (<<)");
419 assert_eq!(TokenKind::DataMarker.display_name(), "data marker (__DATA__ or __END__)");
420 }
421
422 #[test]
425 fn all_returns_132_variants() {
426 assert_eq!(TokenKind::all().len(), 132);
427 assert_eq!(TokenKind::metadata_count(), 132);
428 }
429
430 #[test]
431 fn metadata_round_trips_through_kind() {
432 let m = TokenKind::Sub.metadata();
433 assert_eq!(m.category, TokenCategory::Keyword);
434 assert_eq!(m.display_name, "'sub'");
435 }
436
437 #[test]
440 fn is_assignment_operator_returns_true_for_assign_variants() {
441 assert!(TokenKind::Assign.is_assignment_operator());
442 assert!(TokenKind::PlusAssign.is_assignment_operator());
443 assert!(TokenKind::MinusAssign.is_assignment_operator());
444 assert!(TokenKind::StarAssign.is_assignment_operator());
445 assert!(TokenKind::SlashAssign.is_assignment_operator());
446 assert!(TokenKind::PercentAssign.is_assignment_operator());
447 assert!(TokenKind::DotAssign.is_assignment_operator());
448 assert!(TokenKind::AndAssign.is_assignment_operator());
449 assert!(TokenKind::OrAssign.is_assignment_operator());
450 assert!(TokenKind::XorAssign.is_assignment_operator());
451 assert!(TokenKind::PowerAssign.is_assignment_operator());
452 assert!(TokenKind::LeftShiftAssign.is_assignment_operator());
453 assert!(TokenKind::RightShiftAssign.is_assignment_operator());
454 assert!(TokenKind::LogicalAndAssign.is_assignment_operator());
455 assert!(TokenKind::LogicalOrAssign.is_assignment_operator());
456 assert!(TokenKind::DefinedOrAssign.is_assignment_operator());
457 }
458
459 #[test]
460 fn is_assignment_operator_returns_false_for_non_assign() {
461 assert!(!TokenKind::Plus.is_assignment_operator());
462 assert!(!TokenKind::Equal.is_assignment_operator());
463 assert!(!TokenKind::Identifier.is_assignment_operator());
464 }
465
466 #[test]
467 fn is_logical_operator_returns_true_for_logical_variants() {
468 assert!(TokenKind::And.is_logical_operator());
469 assert!(TokenKind::Or.is_logical_operator());
470 assert!(TokenKind::Not.is_logical_operator());
471 assert!(TokenKind::DefinedOr.is_logical_operator());
472 assert!(TokenKind::WordAnd.is_logical_operator());
473 assert!(TokenKind::WordOr.is_logical_operator());
474 assert!(TokenKind::WordNot.is_logical_operator());
475 assert!(TokenKind::WordXor.is_logical_operator());
476 }
477
478 #[test]
479 fn is_logical_operator_returns_false_for_non_logical() {
480 assert!(!TokenKind::Plus.is_logical_operator());
481 assert!(!TokenKind::Assign.is_logical_operator());
482 assert!(!TokenKind::Identifier.is_logical_operator());
483 }
484
485 #[test]
486 fn is_open_delimiter_returns_true_for_open_delimiters() {
487 assert!(TokenKind::LeftParen.is_open_delimiter());
488 assert!(TokenKind::LeftBrace.is_open_delimiter());
489 assert!(TokenKind::LeftBracket.is_open_delimiter());
490 }
491
492 #[test]
493 fn is_open_delimiter_returns_false_for_non_open() {
494 assert!(!TokenKind::RightParen.is_open_delimiter());
495 assert!(!TokenKind::Semicolon.is_open_delimiter());
496 assert!(!TokenKind::Plus.is_open_delimiter());
497 }
498
499 #[test]
500 fn is_quote_like_returns_true_for_quote_variants() {
501 assert!(TokenKind::Regex.is_quote_like());
502 assert!(TokenKind::Substitution.is_quote_like());
503 assert!(TokenKind::Transliteration.is_quote_like());
504 assert!(TokenKind::QuoteSingle.is_quote_like());
505 assert!(TokenKind::QuoteDouble.is_quote_like());
506 assert!(TokenKind::QuoteWords.is_quote_like());
507 assert!(TokenKind::QuoteCommand.is_quote_like());
508 assert!(TokenKind::HeredocStart.is_quote_like());
509 }
510
511 #[test]
512 fn is_quote_like_returns_false_for_non_quote() {
513 assert!(!TokenKind::String.is_quote_like());
514 assert!(!TokenKind::Identifier.is_quote_like());
515 assert!(!TokenKind::LeftParen.is_quote_like());
516 }
517
518 #[test]
519 fn is_recovery_boundary_returns_true_for_boundaries() {
520 assert!(TokenKind::Semicolon.is_recovery_boundary());
521 assert!(TokenKind::RightParen.is_recovery_boundary());
522 assert!(TokenKind::RightBrace.is_recovery_boundary());
523 assert!(TokenKind::RightBracket.is_recovery_boundary());
524 assert!(TokenKind::Eof.is_recovery_boundary());
525 }
526
527 #[test]
528 fn is_recovery_boundary_returns_false_for_non_boundary() {
529 assert!(!TokenKind::Plus.is_recovery_boundary());
530 assert!(!TokenKind::Identifier.is_recovery_boundary());
531 assert!(!TokenKind::LeftParen.is_recovery_boundary());
532 }
533
534 #[test]
537 fn token_ref_new_checked_rejects_end_before_start() {
538 assert_eq!(
539 TokenRef::new_checked(TokenKind::Identifier, "x", 10, 3),
540 Err(TokenSpanError::EndBeforeStart { start: 10, end: 3 })
541 );
542 }
543
544 #[test]
545 fn token_ref_new_checked_allows_empty_eof() -> Result<(), Box<dyn std::error::Error>> {
546 let tok = TokenRef::new_checked(TokenKind::Eof, "", 7, 7)?;
547 assert_eq!(tok.kind, TokenKind::Eof);
548 assert_eq!(tok.start, 7);
549 assert!(tok.is_empty());
550 Ok(())
551 }
552
553 #[test]
554 fn token_ref_new_checked_allows_empty_unknown() -> Result<(), Box<dyn std::error::Error>> {
555 let tok = TokenRef::new_checked(TokenKind::Unknown, "", 3, 3)?;
556 assert_eq!(tok.kind, TokenKind::Unknown);
557 assert_eq!(tok.start, 3);
558 assert!(tok.is_empty());
559 Ok(())
560 }
561
562 #[test]
563 fn token_ref_new_checked_rejects_empty_non_eof() {
564 assert_eq!(
565 TokenRef::new_checked(TokenKind::Identifier, "", 5, 5),
566 Err(TokenSpanError::EmptySpanNotAllowed { kind: TokenKind::Identifier, at: 5 })
567 );
568 }
569}