Skip to content

Commit 960cb12

Browse files
committed
chore: simplify Comment token emission
Drops the per-comment `src.Slice(...).ToString()` allocation in `ConsumeLineComment` so the Comment token uses `Value = ""` to match the existing Whitespace / Continuation pattern — callers that need the literal text can slice the original input via SourceStart / SourceLength. Also tightens the dispatch comment in BashLexer's outer scan loop and removes redundant `Assert.DoesNotContain` checks in two lexer tests where the existing token-count assertion already proves no Comment token is present.
1 parent 9f78504 commit 960cb12

4 files changed

Lines changed: 26 additions & 34 deletions

File tree

SPEC.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -463,8 +463,10 @@ must handle this.
463463
- The terminating newline is **not** consumed by the Comment token.
464464
It survives as a Whitespace token, preserving statement-boundary
465465
semantics for the parser (see §4).
466-
- A Comment token's `Value` includes the leading `#` for source
467-
fidelity. `SourceStart` / `SourceLength` cover the same range.
466+
- A Comment token's `Value` is empty (matching `Whitespace` /
467+
`Continuation`); `SourceStart` / `SourceLength` identify the slice
468+
including the leading `#` so callers that need the literal text can
469+
recover it from the original input span.
468470
- **Effect on parsing**: comment-only input parses to
469471
`Clauses = []`, `IsUnparseable = false` — mirroring empty-input
470472
behavior. A comment leading, trailing, or interleaved with a clause

src/ShellSyntaxTree/Internal/Bash/Lexing/BashLexer.cs

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -184,15 +184,11 @@ internal static IReadOnlyList<BashToken> Tokenize(string input)
184184
}
185185

186186
// ---- line comment ----
187-
// Reaching this branch means `c` is at a fresh dispatch position
188-
// — whitespace, newlines, operators, quotes, and opaque regions
189-
// are already handled above — so `#` here is at a "word
190-
// boundary" in bash terms and starts a comment to EOL. A `#`
191-
// in the interior of a word never reaches this branch because
192-
// ReadWord consumes the whole word before the outer loop
193-
// resumes. Bash semantics: backslash-escaped `\#` is consumed
194-
// by ReadWord's escape handling and never reaches here either.
195-
// SPEC §5.
187+
// Reaching this branch implies a word boundary (quotes,
188+
// operators, and opaque regions are dispatched above), so `#`
189+
// here starts a comment to EOL. Mid-word `#` is consumed by
190+
// ReadWord, and `\#` by its escape handling — neither reaches
191+
// this point. SPEC §5.
196192
if (c == '#')
197193
{
198194
i = ConsumeLineComment(src, i, tokens);
@@ -411,7 +407,9 @@ private static int ConsumeBacktickSubstitution(
411407
// Consume `#` through (but not including) the next newline. The
412408
// terminating newline stays in the stream so the outer loop emits it
413409
// as a Whitespace token, preserving SPEC §4 clause-boundary
414-
// semantics. The Value retains the leading `#` for source fidelity.
410+
// semantics. Value is "" to match Whitespace/Continuation — callers
411+
// that need the literal text can slice the source via
412+
// SourceStart/SourceLength.
415413
private static int ConsumeLineComment(
416414
ReadOnlySpan<char> src, int start, List<BashToken> tokens)
417415
{
@@ -421,14 +419,8 @@ private static int ConsumeLineComment(
421419
i++;
422420
}
423421

424-
var length = i - start;
425422
tokens.Add(new BashToken(
426-
BashTokenKind.Comment,
427-
src.Slice(start, length).ToString(),
428-
null,
429-
start,
430-
length,
431-
null));
423+
BashTokenKind.Comment, "", null, start, i - start, null));
432424
return i;
433425
}
434426

src/ShellSyntaxTree/Internal/Bash/Lexing/BashTokenKind.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ internal enum BashTokenKind
4141
/// <summary>A bash line comment — <c>#</c> at a word boundary
4242
/// through end-of-line (the terminating newline is preserved as
4343
/// a separate <see cref="Whitespace"/> token so statement
44-
/// boundaries are unaffected). Emitted for source fidelity; the
45-
/// parser drops these in <c>FilterSignificant</c> alongside
44+
/// boundaries are unaffected). Emitted for source fidelity with
45+
/// empty <see cref="BashToken.Value"/>; <see cref="BashToken.SourceStart"/>
46+
/// and <see cref="BashToken.SourceLength"/> identify the slice.
47+
/// The parser drops these in <c>FilterSignificant</c> alongside
4648
/// <see cref="Whitespace"/> and <see cref="Continuation"/>.
4749
/// SPEC §5.</summary>
4850
Comment,

tests/ShellSyntaxTree.Tests/Lexing/BashLexerTests.cs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,6 @@ public void Comment_only_input_lexes_as_single_comment_token()
575575
var tokens = BashLexer.Tokenize("# just a note");
576576
var t = Assert.Single(tokens);
577577
Assert.Equal(BashTokenKind.Comment, t.Kind);
578-
Assert.Equal("# just a note", t.Value);
579578
Assert.Equal(0, t.SourceStart);
580579
Assert.Equal(13, t.SourceLength);
581580
}
@@ -586,7 +585,7 @@ public void Leading_comment_followed_by_newline_and_command_emits_three_signific
586585
// `# fetch\ngit pull` → Comment, Whitespace(\n), Word(git), Whitespace, Word(pull)
587586
var all = BashLexer.Tokenize("# fetch\ngit pull");
588587
Assert.Equal(BashTokenKind.Comment, all[0].Kind);
589-
Assert.Equal("# fetch", all[0].Value);
588+
Assert.Equal(7, all[0].SourceLength);
590589
Assert.Equal(BashTokenKind.Whitespace, all[1].Kind);
591590
Assert.Equal(BashTokenKind.Word, all[2].Kind);
592591
Assert.Equal("git", all[2].Value);
@@ -605,7 +604,8 @@ public void Inline_trailing_comment_does_not_swallow_preceding_word()
605604
Assert.Equal(BashTokenKind.Word, nonWs[1].Kind);
606605
Assert.Equal("pull", nonWs[1].Value);
607606
Assert.Equal(BashTokenKind.Comment, nonWs[2].Kind);
608-
Assert.Equal("# update local", nonWs[2].Value);
607+
Assert.Equal(11, nonWs[2].SourceStart);
608+
Assert.Equal(14, nonWs[2].SourceLength);
609609
}
610610

611611
[Fact]
@@ -625,15 +625,13 @@ public void Hash_in_middle_of_unquoted_word_is_literal_not_comment()
625625
[Fact]
626626
public void Hash_inside_double_quotes_is_literal_not_comment()
627627
{
628+
// The count-equality below would fail if a stray Comment token
629+
// appeared, so a separate DoesNotContain isn't needed.
628630
var tokens = LexNonWs("echo \"hash is #1234\"");
629631
Assert.Equal(2, tokens.Length);
630632
Assert.Equal(BashTokenKind.Word, tokens[0].Kind);
631633
Assert.Equal(BashTokenKind.QuotedString, tokens[1].Kind);
632634
Assert.Equal("hash is #1234", tokens[1].Value);
633-
// No Comment token anywhere.
634-
Assert.DoesNotContain(
635-
BashLexer.Tokenize("echo \"hash is #1234\""),
636-
t => t.Kind == BashTokenKind.Comment);
637635
}
638636

639637
[Fact]
@@ -645,9 +643,6 @@ public void Hash_inside_single_quotes_is_literal_not_comment()
645643
Assert.Equal(BashTokenKind.QuotedString, tokens[1].Kind);
646644
Assert.Equal("use #foo", tokens[1].Value);
647645
Assert.True(tokens[1].IsSingleQuoted);
648-
Assert.DoesNotContain(
649-
BashLexer.Tokenize("echo 'use #foo'"),
650-
t => t.Kind == BashTokenKind.Comment);
651646
}
652647

653648
[Fact]
@@ -676,7 +671,8 @@ public void Comment_starts_immediately_after_operator_without_whitespace()
676671
Assert.Equal(BashTokenKind.Operator, all[2].Kind);
677672
Assert.Equal("&&", all[2].OperatorText);
678673
Assert.Equal(BashTokenKind.Comment, all[3].Kind);
679-
Assert.Equal("# foo", all[3].Value);
674+
Assert.Equal(6, all[3].SourceStart);
675+
Assert.Equal(5, all[3].SourceLength);
680676
}
681677

682678
[Fact]
@@ -686,7 +682,6 @@ public void Comment_at_EOF_without_trailing_newline_terminates_naturally()
686682
// Word(echo), Whitespace, Word(hi), Whitespace, Comment(# done)
687683
Assert.Equal(5, tokens.Count);
688684
Assert.Equal(BashTokenKind.Comment, tokens[4].Kind);
689-
Assert.Equal("# done", tokens[4].Value);
690685
Assert.Equal(14, tokens[4].SourceStart + tokens[4].SourceLength);
691686
}
692687

@@ -697,7 +692,8 @@ public void Comment_does_not_consume_terminating_newline()
697692
// a statement boundary between `# a` and `cmd`.
698693
var tokens = BashLexer.Tokenize("# a\ncmd");
699694
Assert.Equal(BashTokenKind.Comment, tokens[0].Kind);
700-
Assert.Equal("# a", tokens[0].Value);
695+
Assert.Equal(0, tokens[0].SourceStart);
696+
Assert.Equal(3, tokens[0].SourceLength);
701697
Assert.Equal(BashTokenKind.Whitespace, tokens[1].Kind);
702698
// The Whitespace token covers the newline.
703699
Assert.Equal(3, tokens[1].SourceStart);

0 commit comments

Comments
 (0)