Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions src/Formatter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,167 @@ end sub`;
it('works for arrays with objects in them on separate lines', () => {
formatEqual(`theVar = [\n {\n name = "bob"\n }\n]`);
});

// https://github.com/rokucommunity/brighterscript-formatter/issues/85
// When `{` and `function` appear on the same line, each independently triggers an indent,
// causing the body to be triple-indented instead of double-indented.
// The fix uses lookahead: if a line opens a bracket scope and a function/sub on the same
// line, and both close together on a later line, only 1 additional indent level is applied.
it('does not double-indent when { and function are on the same line', () => {
expect(formatter.format(undent`
sub main()
key_9 = { runtimeCheck: function() as boolean
return true
end function }
end sub
`, { formatMultiLineObjectsAndArrays: false })).to.equal(undent`
sub main()
key_9 = { runtimeCheck: function() as boolean
return true
end function }
end sub
`);
});

it('does not double-indent when [ and function are on the same line', () => {
expect(formatter.format(undent`
sub main()
items = [function() as boolean
return true
end function]
end sub
`, { formatMultiLineObjectsAndArrays: false })).to.equal(undent`
sub main()
items = [function() as boolean
return true
end function]
end sub
`);
});

it('does not double-indent inline AA with function value nested inside outer AA (issue #85)', () => {
expect(formatter.format(undent`
sub main()
config = {
key_1: "value1"
key_9: { env: ["any"], runtimeCheck: function() as boolean
return true
end function }
}
end sub
`, { formatMultiLineObjectsAndArrays: false })).to.equal(undent`
sub main()
config = {
key_1: "value1"
key_9: { env: ["any"], runtimeCheck: function() as boolean
return true
end function }
}
end sub
`);
});

it('does not double-indent multiple inline function values in the same AA (issue #85)', () => {
expect(formatter.format(undent`
sub main()
config = {
key_9: { runtimeCheck: function() as boolean
return true
end function }
key_10: { runtimeCheck: function() as boolean
return false
end function }
}
end sub
`, { formatMultiLineObjectsAndArrays: false })).to.equal(undent`
sub main()
config = {
key_9: { runtimeCheck: function() as boolean
return true
end function }
key_10: { runtimeCheck: function() as boolean
return false
end function }
}
end sub
`);
});

it('does not double-indent when deeply nested (issue #85)', () => {
expect(formatter.format(undent`
namespace tests
class TestStuff
function test()
m.call({
key_9: { runtimeCheck: function() as boolean
return true
end function }
})
end function
end class
end namespace
`, { formatMultiLineObjectsAndArrays: false })).to.equal(undent`
namespace tests
class TestStuff
function test()
m.call({
key_9: { runtimeCheck: function() as boolean
return true
end function }
})
end function
end class
end namespace
`);
});

it('empty lines inside [{ }] do not break surrounding indentation', () => {
formatEqualTrim(`
sub test()
array = [{

}]
if true then
print true
end if
end sub
`);
});

// https://github.com/rokucommunity/brighterscript-formatter/issues/85
it('does not double-indent when { and sub are on the same line (issue #85)', () => {
expect(formatter.format(undent`
sub main()
m.foo = { callback: sub()
doThing()
end sub }
end sub
`, { formatMultiLineObjectsAndArrays: false })).to.equal(undent`
sub main()
m.foo = { callback: sub()
doThing()
end sub }
end sub
`);
});

it('does not apply double-indent fix when bracket closer has non-end-function token before it (issue #85)', () => {
// When `}` has something other than `end function`/`end sub` immediately before it
// on the same line (e.g. a second AA entry), the fix intentionally does not apply.
expect(formatter.format(undent`
sub main()
m.foo = { cb: function()
return true
end function, other: 1 }
end sub
`, { formatMultiLineObjectsAndArrays: false })).to.equal(undent`
sub main()
m.foo = { cb: function()
return true
end function, other: 1 }
end sub
`);
});
});

describe('indentSpaceCount', () => {
Expand Down Expand Up @@ -1226,6 +1387,45 @@ end sub`;
let program = `if (request.AsyncGetToString())\n scope.immediatelyFailed = false\nelse\n scope.immediatelyFailed = true\nend if`;
expect(formatter.format(program)).to.equal(program);
});

// https://github.com/rokucommunity/brighterscript-formatter/issues/82
it('does not de-indent when ".catch" is used as a method call (issue #82)', () => {
formatEqual(undent`
sub main()
if true then
m.promise.catch(sub(err)
print err
end sub)
end if
end sub
`);
});

it('does not de-indent surrounding if blocks when chaining lambdas with typecast args (issue #82)', () => {
formatEqual(undent`
sub main()
if true then
m.list.filter(function(item as object) as boolean
return item.active = true
end function).forEach(sub(item as object)
print item
end sub)
end if
end sub
`);
});

it('does not de-indent when lambda has a typed parameter in a method chain (issue #82)', () => {
formatEqual(undent`
sub main()
if true then
someObject.next(function(val as integer) as string
return val.toStr()
end function)
end if
end sub
`);
});
});

describe('typeCase', () => {
Expand Down
56 changes: 54 additions & 2 deletions src/formatters/IndentFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ export class IndentFormatter {

let parentIndentTokenKinds: TokenKind[] = [];

// Tracks bracket-closer tokens (e.g. `}` or `]`) that should be skipped when
// computing indentation offsets. Used to prevent double-indenting when a bracket
// opener and a function/sub keyword appear on the same line and both close together
// (e.g. `{ function() as boolean\n return true\nend function }`).
const skipOutdentTokens = new Set<Token>();

//the list of output tokens
let result: Token[] = [];

//set the loop to run for a max of double the number of tokens we found so we don't end up with an infinite loop
for (let lineTokens of this.splitTokensByLine(tokens)) {
const { currentLineOffset, nextLineOffset } = this.processLine(lineTokens, tokens, ifStatements, parentIndentTokenKinds);
const { currentLineOffset, nextLineOffset } = this.processLine(lineTokens, tokens, ifStatements, parentIndentTokenKinds, skipOutdentTokens);

//uncomment the next line to debug indent/outdent issues
// console.log(currentLineOffset.toString().padStart(3, ' '), nextLineOffset.toString().padStart(3, ' '), lineTokens.map(x => x.text).join('').replace(/\r?\n/, '').replace(/^\s*/, ''));
Expand All @@ -50,7 +56,8 @@ export class IndentFormatter {
lineTokens: Token[],
tokens: Token[],
ifStatements: Map<Token, IfStatement>,
parentIndentTokenKinds: TokenKind[]
parentIndentTokenKinds: TokenKind[],
skipOutdentTokens: Set<Token>
): { currentLineOffset: number; nextLineOffset: number } {
const getParentIndentTokenKind = () => {
const parentIndentTokenKind = parentIndentTokenKinds.length > 0 ? parentIndentTokenKinds[parentIndentTokenKinds.length - 1] : undefined;
Expand All @@ -61,6 +68,10 @@ export class IndentFormatter {
let nextLineOffset = 0;
let foundIndentorThisLine = false;
let firstNonWhitespaceToken: Token | null = null;
// Tracks the last multi-line bracket opener (`{` or `[`) and its closer pushed as an indent
// on this line. Storing the closer avoids a redundant getClosingToken call during compound
// indent detection.
let lastBracketIndentInfo: { opener: Token; closer: Token } | null = null;

for (let i = 0; i < lineTokens.length; i++) {
let token = lineTokens[i];
Expand Down Expand Up @@ -157,6 +168,8 @@ export class IndentFormatter {
parentIndentTokenKinds.push(token.kind);

//don't double indent if this is `[[...\n...]]` or `[{...\n...}]`
// Note: this block is before the lastBracketIndentTokenOnLine update below so
// that skipped `{`/`[` tokens don't overwrite the tracked bracket.
if (
//is open square
token.kind === TokenKind.LeftSquareBracket &&
Expand All @@ -175,6 +188,37 @@ export class IndentFormatter {
i++;
}
}

// Track the last multi-line bracket opener on this line for compound-indent detection below.
// Only track brackets whose closer is on a different line — single-line brackets like
// `["any"]` open and close on the same line and don't affect body indentation.
if (token.kind === TokenKind.LeftCurlyBrace || token.kind === TokenKind.LeftSquareBracket) {
const bCloseKind = token.kind === TokenKind.LeftCurlyBrace ? TokenKind.RightCurlyBrace : TokenKind.RightSquareBracket;
const bCloser = util.getClosingToken(tokens, tokens.indexOf(token), token.kind, bCloseKind);
if (bCloser && bCloser.range.start.line !== token.range.start.line) {
lastBracketIndentInfo = { opener: token, closer: bCloser };
}
}

// Don't double-indent when a bracket opener (`{` or `[`) and a function/sub keyword
// both appear on the same line and both close together on a later line, e.g.:
// key: { runtimeCheck: function() as boolean
// return true <-- should be 1 level deeper, not 2
// end function }
if (
foundIndentorThisLine &&
CallableKeywordTokenKinds.includes(token.kind) &&
lastBracketIndentInfo !== null
) {
const { closer: bracketCloser } = lastBracketIndentInfo;
const tokenBeforeCloser = util.getPreviousNonWhitespaceToken(tokens, tokens.indexOf(bracketCloser), true);
if (tokenBeforeCloser && (tokenBeforeCloser.kind === TokenKind.EndFunction || tokenBeforeCloser.kind === TokenKind.EndSub)) {
// The function closes inside the bracket — undo the extra indent level and
// mark the bracket closer to be skipped on the closing line
nextLineOffset--;
skipOutdentTokens.add(bracketCloser);
}
}
} else if (this.isOutdentToken(token, nextNonWhitespaceToken)) {
//do not un-indent if this is a `next` or `endclass` token preceeded by a period
if (
Expand All @@ -184,6 +228,14 @@ export class IndentFormatter {
continue;
}

// This bracket closer was marked to be skipped by the compound-indent fix above.
// Pop the stack to keep it balanced but don't adjust offsets.
if (skipOutdentTokens.has(token)) {
skipOutdentTokens.delete(token);
parentIndentTokenKinds.pop();
continue;
}

nextLineOffset--;
if (foundIndentorThisLine === false) {
currentLineOffset--;
Expand Down