diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d2208c..049b905 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,14 @@ on: - master pull_request: +concurrency: + # We use the pull request number to ensure that each PR has a unique concurrency group. This prevents different PRs from interfering + # with each other, especially when multiple PRs originate from the same branch. If the pull request number is not available, we fall + # back to using the branch name (`github.head_ref`) or the full reference (`github.ref`), ensuring uniqueness. + # + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.head_ref || github.ref }} + cancel-in-progress: true + jobs: ci: runs-on: ${{ matrix.os }} @@ -12,7 +20,7 @@ jobs: NODE_OPTIONS: "--max-old-space-size=4096" strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest] steps: - uses: actions/checkout@master - uses: actions/setup-node@master diff --git a/src/formatters/MultiLineItemFormatter.spec.ts b/src/formatters/MultiLineItemFormatter.spec.ts index 3968d67..817ee5d 100644 --- a/src/formatters/MultiLineItemFormatter.spec.ts +++ b/src/formatters/MultiLineItemFormatter.spec.ts @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { Formatter } from '../Formatter'; +import { undent } from 'undent'; describe('MultiLineItemFormatter', () => { let formatter: Formatter; @@ -15,6 +16,27 @@ describe('MultiLineItemFormatter', () => { expect(formatter.format(inputSameLine).trim()).to.equal(expectedSameLine.trim()); }); + it('preserves isMatchingDoubleArrayOrArrayCurly [[ .. ]]', () => { + const input = undent` + [[1, 2, 3] + ]`; + const expected = undent` + [ + [1, 2, 3] + ]`; + expect(formatter.format(input).trim()).to.equal(expected.trim()); + }); + it('preserves isMatchingDoubleArrayOrArrayCurly [{ .. }]', () => { + const input = undent` + [{1, 2, 3} + ]`; + const expected = undent` + [ + { 1, 2, 3 } + ]`; + expect(formatter.format(input).trim()).to.equal(expected.trim()); + }); + it('preserves return object on same line (multi-line content)', () => { const input = `sub foo()\n return {\n a: 1\n }\nend sub`; const expected = `sub foo()\n return {\n a: 1\n }\nend sub`; @@ -26,4 +48,20 @@ describe('MultiLineItemFormatter', () => { const expected = `sub foo()\n return { a: 1,\n b: 2\n }\nend sub`; expect(formatter.format(input).trim()).to.equal(expected.trim()); }); + + it('preserves multiple open/close pairs that are unbalanced', () => { + const input = undent` + m.callback.Invoke([m, { + "event": event, + "data": data, + }]) + `; + const expected = undent` + m.callback.Invoke([m, { + "event": event, + "data": data, + }]) + `; + expect(formatter.format(input).trim()).to.equal(expected.trim()); + }); }); diff --git a/src/formatters/MultiLineItemFormatter.ts b/src/formatters/MultiLineItemFormatter.ts index 17faef5..d53540a 100644 --- a/src/formatters/MultiLineItemFormatter.ts +++ b/src/formatters/MultiLineItemFormatter.ts @@ -33,7 +33,9 @@ export class MultiLineItemFormatter { //is NOT array like `[[ ...\n ]]`, or `[{ ...\n }]`) !this.isMatchingDoubleArrayOrArrayCurly(tokens, i) && //Don't reformat if the opening bracket or curly is on the same line as 'return' - !this.isReturnArrayOrCurlyOnSameLine(tokens, i) + !this.isReturnArrayOrCurlyOnSameLine(tokens, i) && + //Don't reformat if there are multiple open/close pairs that are unbalanced + !this.isMultiUnbalancedOpenClosePair(tokens, i) ) { tokens.splice(i + 1, 0, { kind: TokenKind.Newline, @@ -79,6 +81,38 @@ export class MultiLineItemFormatter { return false; } + /** + * Detects when a line has both unbalanced square brackets and curly braces, + * which indicates a pattern like `[m, {` that should not be reformatted. + * Scans tokens from the given index until a newline is found. + * @param tokens The array of tokens to scan. + * @param currentIndex The index to start scanning from. + * @returns {boolean} True if both curly and square brackets are unbalanced before a newline; otherwise, false. + */ + public isMultiUnbalancedOpenClosePair(tokens: Token[], currentIndex: number) { + let curlyOpenCount = 0; + let squareOpenCount = 0; + + for (let i = currentIndex; i < tokens.length; i++) { + let token = tokens[i]; + if (token.kind === TokenKind.Newline) { + break; + } + + if (token.kind === TokenKind.LeftCurlyBrace) { + curlyOpenCount++; + } else if (token.kind === TokenKind.RightCurlyBrace) { + curlyOpenCount--; + } else if (token.kind === TokenKind.LeftSquareBracket) { + squareOpenCount++; + } else if (token.kind === TokenKind.RightSquareBracket) { + squareOpenCount--; + } + } + + return curlyOpenCount > 0 && squareOpenCount > 0; + } + public isMatchingDoubleArrayOrArrayCurly(tokens: Token[], currentIndex: number) { let token = tokens[currentIndex]; let nextNonWhitespaceToken = util.getNextNonWhitespaceToken(tokens, currentIndex, true); @@ -94,11 +128,11 @@ export class MultiLineItemFormatter { let closingToken = util.getClosingToken(tokens, currentIndex, TokenKind.LeftSquareBracket, TokenKind.RightSquareBracket); //look at the previous token let previous = closingToken && util.getPreviousNonWhitespaceToken(tokens, tokens.indexOf(closingToken), true); - /* istanbul ignore else (because I can't figure out how to make this happen but I think it's still necessary) */ if (previous && (previous.kind === TokenKind.RightSquareBracket || previous.kind === TokenKind.RightCurlyBrace)) { return true; } } + return false; } /**