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