Skip to content

Add new formatting options#124

Open
chrisdp wants to merge 16 commits into
masterfrom
feature/new-formatting-options
Open

Add new formatting options#124
chrisdp wants to merge 16 commits into
masterfrom
feature/new-formatting-options

Conversation

@chrisdp

@chrisdp chrisdp commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds 7 new formatting options: alignAssignments, blankLinesBetweenFunctions, inlineArrayAndObjectThreshold, maxConsecutiveEmptyLines, removeBlankLinesAtStartOfBlock, singleLineIf, and trailingComma
  • Extends trailingComma to apply to all items in multiline arrays and AAs (not just the last), enabling conversion between BrightScript's newline-separator style and comma-separator style
  • Adds trailingComma: 'allButLast' mode — commas on every item except the trailing one (conventional style)
  • Documents all previously undocumented bsfmt.json options in the README

Test plan

  • All existing tests pass (npm test)
  • New tests added for trailingComma covering: comma-free AA → 'always', 'never' removing all commas, 'allButLast', nested collections, blank lines inside collections
  • New tests added for each of the 7 new formatting options

🤖 Generated with Claude Code

chrisdp and others added 5 commits March 28, 2026 19:44
- maxConsecutiveEmptyLines: collapse runs of blank lines to a configurable max
- trailingComma: add or remove trailing commas in multi-line arrays/AAs
- blankLinesBetweenFunctions: enforce exact blank lines between function/sub declarations
- singleLineIf: collapse or expand single-statement if blocks
- inlineArrayAndObjectThreshold: collapse short multi-line arrays/AAs to a single line
- removeBlankLinesAtStartOfBlock: remove blank lines at the start of function/if/for/while bodies
- alignAssignments: align = signs in consecutive assignment blocks

Each option is covered by tests written before implementation (TDD). Pipeline
ordering comments explain why each formatter runs where it does relative to
IndentFormatter and MultiLineItemFormatter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 'allButLast' mode

- 'always' and 'never' now apply to every item in a multiline collection,
  not just the trailing one — enabling conversion between the BrightScript
  newline-separator style and the comma-separator style
- New 'allButLast' mode adds commas to all items except the last, matching
  the conventional style used in most languages
- Formatter processes collections innermost-first to keep indices stable
  across splices, and collects all item positions before deciding so the
  last item can be identified for 'allButLast'

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added entries for insertSpaceAfterConditionalCompileSymbol,
trailingComma, maxConsecutiveEmptyLines, blankLinesBetweenFunctions,
singleLineIf, inlineArrayAndObjectThreshold, removeBlankLinesAtStartOfBlock,
and alignAssignments — all of which existed in FormattingOptions but were
absent from the options table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread README.md Outdated
@chrisdp chrisdp marked this pull request as ready for review March 29, 2026 01:19
@chrisdp chrisdp changed the title Add new formatting options and improve trailingComma Add new formatting options Mar 29, 2026

@TwitchBronBron TwitchBronBron left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got some bugs, yo.

Comment on lines +35 to +36
const nonWs = lineTokens.filter(t => t.kind !== TokenKind.Whitespace && t.kind !== TokenKind.Newline && t.kind !== TokenKind.Eof);
return nonWs.length >= 3 && nonWs[0].kind === TokenKind.Identifier && nonWs[1].kind === TokenKind.Equal;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that this is the right approach. We will iterate over every single token, just to see if we have more than 3? That's seemingly wasteful

Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread README.md Outdated
chrisdp and others added 7 commits May 8, 2026 08:27
Will be revisited later with a more eslint-like/flexible API.
Replace `expand`/`collapse`/`original` with eslint-style values:
`block`, `inline`, `inlineNoElseIf`, `inlineNoElse`, `original`. The
inline* variants form a strictness gradient over which else/else-if
branch shapes may collapse.

Block mode (expand) now walks the entire if/else-if/else chain via
elseBranch and breaks around every then/else, so inline ifs with else
branches expand correctly instead of cramming the whole chain onto one
body line. Filters on `isInline === true` (not `!tokens.endIf`) to
avoid mistaking a multi-line chain's outer IfStatement for an inline
one — brighterscript attaches the `end if` to the deepest else-if of
a multi-line chain, leaving the outer endIf-less. Also rejects inline
ifs whose range spans multiple lines (e.g. `if x then return { ... }`
with a multi-line literal body) to avoid landing `end if` inside the
literal.

Inline modes (collapse) reject bodies that would make the result
broken or undesirable: nested IfStatement, TryCatchStatement,
CommentStatement, and any single-statement body whose range spans
multiple lines (multi-line for/while/for-each, multi-line function
call, multi-line array/AA literal as RHS, etc).
Both modes were previously declared in the type union but routed to
the same code path as inlineNoElse. They now do what their names imply:

- `inlineNoElseIf` collapses simple `if/then` and adds `if/then/else`,
  but rejects `else if` chains.
- `inline` adds `else if` chains (with optional final `else`), the most
  aggressive setting.

isCollapsible walks the elseBranch chain and validates each branch's
body in turn, with the mode controlling which branch shapes are
allowed. collapse() walks the same chain to gather every body opener
(then tokens, plus plain-else's else) and every body closer (else
tokens, plus the chain's end-if), then collapses each `\n + indent`
into a single space. Token references stay valid across splices, so
operations are order-independent and re-resolve indices each time.

Removed two SingleLineIfFormatter unit tests that probed early-return
paths of the old single-pass collapse; the user-facing tests in
Formatter.spec.ts cover the same edge cases (corrupt structure now
short-circuits via an explicit pre-mutation validation pass).
Replace `inlineArrayAndObjectThreshold: number` with eslint-style
string values: `always`, `never`, `fitsLine`, `original`. Add a global
`maxLineLength` option that `fitsLine` consults to decide whether the
collapsed line stays within budget; falls back to `always` when unset.

Collapse path:
- Inserts a comma + space between items when the original separator was
  only a newline (previously produced broken output like `{ a: 1b: 2 }`
  or `[123]`).
- Refuses to collapse literals whose contents include any of: line
  comments (`'note`), `bs:disable-line` directives, conditional-compile
  directives (`#if`/`#else if`/`#else`/`#end if`), nested multi-line
  literals, items whose value spans multiple physical lines (multi-line
  function expressions, etc), or regex literals (brighterscript's lexer
  does not include `,` in `PreceedingRegexTypes`, so a regex after a
  comma re-lexes as division).
- For `fitsLine`, factors visual indent into the length budget using
  `indentSpaceCount` for tab expansion.

Expand path (new for `never` mode):
- Walks single-line literals with at least 2 items, inserts newlines
  after the opener, after each top-level comma, and before the closer.
  IndentFormatter restores indentation on the next pass.
- Skips empty literals and single-element literals.

Tests cover all four modes, every structural rejection, comma-fix bugs,
and length cutoffs. Validated semantically against bsc on
jellyfin-roku, jellyfin-roku-legacy, jellyrock, and Roku-GooglePhotos
across 3 modes — all produce identical bsc diagnostic histograms.
Replace the boolean `removeBlankLinesAtStartOfBlock` option with
`blockSpacing`, which controls blank-line spacing AROUND block
constructs (not just at the start of bodies).

String form:
- `'before'`  — blank line above the block (above any leading comment
  or annotation)
- `'after'`   — blank line below the block (after any same-line
  trailing comment)
- `'between'` — both `'before'` and `'after'`
- `'always'`  — `'between'` plus inner-body padding (start and end of
  the body)
- `'original'` (or omitted) — leave spacing as written

Object form allows per-construct overrides for function, sub, if, for,
while, try, plus a `default` fallback for unspecified constructs.
Setting `if` covers the entire if/else if/else chain. `for` covers
`for` and `for each`. `try` covers the entire try/catch construct.

Comment and annotation attachment: leading line comments and
annotations (`@deprecated`, etc.) immediately above an opener are
part of the block. The `'before'` blank lands above the entire
preamble, never between preamble and opener.

Parent-boundary detection: spacing is suppressed when the construct
is the first or last thing in its parent's body. Parents recognized
include function/sub bodies, if/else branches, for/while/try bodies,
namespace bodies, class/interface/enum bodies, and conditional-compile
branches.

Anonymous function/sub expressions (`.catch(sub(e) ... end sub)`,
`x = function() ... end function`) are skipped entirely — they're
expression-form, not declarations.

The fix also handles nested same-kind constructs (depth-counted
closer matching so nested if/end if pairs resolve correctly) and
correctly skips probe whitespace when checking if a closer is the
parent's closer.

Tests now cover: every string value, per-construct overrides, default
fallback, comment + annotation preambles, parent boundary suppression
(first child of namespace, last child of namespace, nested-only-child
patterns), anonymous lambda exclusion, and nested namespaces.

Validated against bsc on five Roku repos × ten configurations (four
string values + six per-construct overrides): 50/50 produce identical
bsc diagnostic histograms.
Restructure the new formatters so every branch is either exercised by
a real test or genuinely unreachable from contract guarantees:

- Remove defensive guards that the upstream call shape already
  guarantees (e.g., `indexOf === -1` checks where the validation pass
  has already proven the token is present).
- Replace optional chaining (`?.`) with non-null assertions (`!`)
  where the operand is provably defined.
- Restructure `isCollapsible`'s walk loop as `while (true)` since
  every body path either returns or continues — eliminates the
  unreachable post-loop return.
- Use `tokens.splice(closeIdx - 1, 1, newline)` directly where the
  preceding ws token is guaranteed by the lexer, etc.

Add unit tests for branches reachable only through synthetic token
arrays:

- BlockSpacingFormatter.spec.ts (new file) — 29 unit tests targeting
  defensive paths and nested same-kind closer matching.
- InlineArrayAndObjectFormatter.spec.ts — extended with tests for
  nested function expressions (depth >= 2), Sub/EndSub matching,
  unterminated function in literal, and `never`-mode unclosed brackets.
- SingleLineIfFormatter.spec.ts — added a synthetic chain where the
  `else` token is not preceded by whitespace.

Add functional tests for paths reachable through normal BS input:

- Tab-indent visual-width handling under fitsLine
- Explicit indentSpaceCount setting
- Nested single-line arrays/AAs in `never` mode (recursive expansion)
- `try` construct via object form
- Parent-boundary detection in namespaces and class methods
- Mixed comment + annotation preamble walking
- Anonymous lambda exclusion (subs in `.catch(sub(e) ...)` chains)
- Nested namespace scenarios

Final coverage: 100% lines, branches, functions, statements across
all source files. Validated against bsc on four public repos × ten
configurations — 40/40 produce identical diagnostic histograms.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants