From 1fe1bb5e70ab0bf66f277f1f61d3a037f3066428 Mon Sep 17 00:00:00 2001 From: Rob Reed Date: Fri, 28 Nov 2025 14:14:06 -0800 Subject: [PATCH 1/2] Improve and simplify insertSpaceAroundParameterAssignment No more brittle logic by tying into FunctionParameterExpression which also handles anonymous functions properly. --- .../InteriorWhitespaceFormatter.spec.ts | 308 ++++++++++++------ src/formatters/InteriorWhitespaceFormatter.ts | 88 ++--- 2 files changed, 236 insertions(+), 160 deletions(-) diff --git a/src/formatters/InteriorWhitespaceFormatter.spec.ts b/src/formatters/InteriorWhitespaceFormatter.spec.ts index 9128ed6..46818d5 100644 --- a/src/formatters/InteriorWhitespaceFormatter.spec.ts +++ b/src/formatters/InteriorWhitespaceFormatter.spec.ts @@ -7,7 +7,7 @@ import { util } from '../util'; import { InteriorWhitespaceFormatter } from './InteriorWhitespaceFormatter'; import type { FormattingOptions } from '../FormattingOptions'; -describe('insertSpaceAroundParameterAssignment', () => { +describe('interiorWhitespaceFormatter', () => { let interiorWhitespaceFormatter: InteriorWhitespaceFormatter; beforeEach(() => { @@ -40,90 +40,6 @@ describe('insertSpaceAroundParameterAssignment', () => { expect(format(input)).to.equal(expected); }); - it('formats empty objects in function parameters correctly with explicit option false', () => { - const input = undent` - sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions={} as object) - print "hello" - end sub - `; - const expected = undent` - sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions = {} as object) - print "hello" - end sub - `; - expect(format(input, { insertSpaceBetweenEmptyCurlyBraces: false })).to.equal(expected); - }); - - it('formats empty objects in function parameters correctly with explicit option true', () => { - const input = undent` - sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions={} as object) - print "hello" - end sub - `; - const expected = undent` - sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions = { } as object) - print "hello" - end sub - `; - expect(format(input, { insertSpaceBetweenEmptyCurlyBraces: true })).to.equal(expected); - }); - - it('formats empty objects in function parameters correctly with insertSpaceAroundParameterAssignment: false', () => { - const input = undent` - sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions = {} as object) - print "hello" - end sub - `; - const expected = undent` - sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions={} as object) - print "hello" - end sub - `; - expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); - }); - - it('handles nested parentheses in function parameters correctly', () => { - const input = undent` - sub foo(a = (1 + 2), b = 3) - print "hello" - end sub - `; - const expected = undent` - sub foo(a=(1 + 2), b=3) - print "hello" - end sub - `; - expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); - }); - - it('handles simple assignments in function parameters correctly', () => { - const input = undent` - sub foo(a = 1, b = 3) - print "hello" - end sub - `; - const expected = undent` - sub foo(a=1, b=3) - print "hello" - end sub - `; - expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); - }); - - it('handles optional chaining in default parameter values', () => { - const input = undent` - sub foo(a = m?.call?()) - print "hello" - end sub - `; - const expected = undent` - sub foo(a=m?.call?()) - print "hello" - end sub - `; - expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); - }); - it('handles AA literal with key and colon on same line', () => { const input = undent` sub main() @@ -144,24 +60,6 @@ describe('insertSpaceAroundParameterAssignment', () => { expect(format(input)).to.equal(input); }); - it('handles anonymous function with insertSpaceAroundParameterAssignment: false', () => { - const input = undent` - sub main() - a = sub(a=1) - end sub - end sub - `; - expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(input); - }); - - it('handles parameter assignment without spaces with insertSpaceAroundParameterAssignment: false', () => { - const input = undent` - sub main(a=1) - end sub - `; - expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(input); - }); - it('handles sub with invalid next token', () => { const input = undent` sub = @@ -256,4 +154,208 @@ describe('insertSpaceAroundParameterAssignment', () => { const input = 'sub main ='; expect(format(input)).to.equal(input); }); + + describe('insertSpaceBetweenEmptyCurlyBraces', () => { + it('formats empty objects in function parameters correctly with explicit option false', () => { + const input = undent` + sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions={} as object) + print "hello" + end sub + `; + const expected = undent` + sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions = {} as object) + print "hello" + end sub + `; + expect(format(input, { insertSpaceBetweenEmptyCurlyBraces: false })).to.equal(expected); + }); + + it('formats empty objects in function parameters correctly with explicit option true', () => { + const input = undent` + sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions={} as object) + print "hello" + end sub + `; + const expected = undent` + sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions = { } as object) + print "hello" + end sub + `; + expect(format(input, { insertSpaceBetweenEmptyCurlyBraces: true })).to.equal(expected); + }); + }); + + describe('insertSpaceAroundParameterAssignment()', () => { + it('handles anonymous function with insertSpaceAroundParameterAssignment: false', () => { + const input = undent` + sub main() + a = sub(a=1) + end sub + end sub + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(input); + }); + + it('handles parameter assignment without spaces with insertSpaceAroundParameterAssignment: false', () => { + const input = undent` + sub main(a=1) + end sub + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(input); + }); + + it('adds spaces around parameter assignment when insertSpaceAroundParameterAssignment is true (default)', () => { + const input = undent` + sub main(a=1) + end sub + `; + const expected = undent` + sub main(a = 1) + end sub + `; + expect(format(input, { insertSpaceAroundParameterAssignment: true })).to.equal(expected); + }); + + it('handles function parameter assignment without spaces with insertSpaceAroundParameterAssignment: false', () => { + const input = undent` + function main(a = 1) + end function + `; + const expected = undent` + function main(a=1) + end function + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); + }); + + it('handles function with no default values having insertSpaceAroundParameterAssignment: false', () => { + const input = undent` + function longInteger(a as boolean) + end function + `; + const expected = undent` + function longInteger(a as boolean) + end function + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); + }); + + it('handles function parameter assignment with spaces with insertSpaceAroundParameterAssignment: false', () => { + const input = undent` + function longInteger(a = 1, b = 2) + end function + `; + const expected = undent` + function longInteger(a=1, b=2) + end function + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); + }); + + it('handles anonymous function parameter assignment without spaces with insertSpaceAroundParameterAssignment: false', () => { + const input = undent` + a = function(a = 1) + end function + `; + const expected = undent` + a = function(a=1) + end function + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); + }); + + it('handles complex assignment without spaces with insertSpaceAroundParameterAssignment: false', () => { + const input = undent` + sub init() + uiResolution = {}; + + m.global.AddFields({ + "mainInit": false, + "exitChannel": false, + "hasExitedChannel": false, + + ' UI resolution fields. + "uiResolution": uiResolution, + "isFHD": (uiResolution.name = "FHD"), + "isHD": (uiResolution.name = "HD"), + "isSD": (uiResolution.name = "SD"), + }) + end sub + `; + const expected = undent` + sub init() + uiResolution = {}; + + m.global.AddFields({ + "mainInit": false, + "exitChannel": false, + "hasExitedChannel": false, + + ' UI resolution fields. + "uiResolution": uiResolution, + "isFHD": (uiResolution.name = "FHD"), + "isHD": (uiResolution.name = "HD"), + "isSD": (uiResolution.name = "SD"), + }) + end sub + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); + }); + + it('formats empty objects in function parameters correctly with insertSpaceAroundParameterAssignment: false', () => { + const input = undent` + sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions = {} as object) + print "hello" + end sub + `; + const expected = undent` + sub PlaybackSession_MakeDecisionAndSetContent(decisionOptions={} as object) + print "hello" + end sub + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); + }); + + it('handles nested parentheses in function parameters correctly', () => { + const input = undent` + sub foo(a = (1 + 2), b = 3) + print "hello" + end sub + `; + const expected = undent` + sub foo(a=(1 + 2), b=3) + print "hello" + end sub + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); + }); + + it('handles simple assignments in function parameters correctly', () => { + const input = undent` + sub foo(a = 1, b = 3) + print "hello" + end sub + `; + const expected = undent` + sub foo(a=1, b=3) + print "hello" + end sub + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); + }); + + it('handles optional chaining in default parameter values', () => { + const input = undent` + sub foo(a = m?.call?()) + print "hello" + end sub + `; + const expected = undent` + sub foo(a=m?.call?()) + print "hello" + end sub + `; + expect(format(input, { insertSpaceAroundParameterAssignment: false })).to.equal(expected); + }); + }); }); diff --git a/src/formatters/InteriorWhitespaceFormatter.ts b/src/formatters/InteriorWhitespaceFormatter.ts index 427c57f..9e448e6 100644 --- a/src/formatters/InteriorWhitespaceFormatter.ts +++ b/src/formatters/InteriorWhitespaceFormatter.ts @@ -1,4 +1,4 @@ -import type { AALiteralExpression, AAMemberExpression, Parser, Token } from 'brighterscript'; +import type { AALiteralExpression, AAMemberExpression, FunctionParameterExpression, Parser, Token } from 'brighterscript'; import { createVisitor, WalkMode, TokenKind } from 'brighterscript'; import type { TokenWithStartIndex } from '../constants'; import { TokensBeforeNegativeNumericLiteral, NumericLiteralTokenKinds, ConditionalCompileTokenKinds } from '../constants'; @@ -222,62 +222,6 @@ export class InteriorWhitespaceFormatter { for (i; i < tokens.length; i++) { setIndex(i); - // Special handling for the interior of functions (spacing between function assignment operators) - // When insertSpaceAroundParameterAssignment is false, remove spaces around parameter assignment operators - /* istanbul ignore if (valid and tested, but missing some coverage and hard to test) */ - if (options.insertSpaceAroundParameterAssignment === false && (token.kind === TokenKind.Sub || token.kind === TokenKind.Function)) { - const nextToken = util.getNextNonWhitespaceToken(tokens, i); - let parenToken: Token | undefined; - - // Detect immediate '(' after keyword or function name - if (nextToken?.kind === TokenKind.LeftParen) { - parenToken = nextToken; - } else if (nextToken?.kind === TokenKind.Identifier) { - const afterNext = util.getNextNonWhitespaceToken(tokens, tokens.indexOf(nextToken)); - if (afterNext?.kind === TokenKind.LeftParen) { - parenToken = afterNext; - } - } - - if (parenToken) { - // Iterate tokens inside parentheses and trim whitespace around assignment operators - // Track nested parentheses to find the correct closing paren - let j = tokens.indexOf(parenToken) + 1; - let parenDepth = 1; - const indicesToRemove: number[] = []; - while (j < tokens.length && parenDepth > 0) { - const t = tokens[j]; - - // Track nested parentheses - if (t.kind === TokenKind.LeftParen || t.kind === TokenKind.QuestionLeftParen) { - parenDepth++; - } else if (t.kind === TokenKind.RightParen) { - parenDepth--; - if (parenDepth === 0) { - break; - } - } - - if (t.kind === TokenKind.Equal) { - // Remove left whitespace - if (j > 0 && tokens[j - 1].kind === TokenKind.Whitespace) { - indicesToRemove.push(j - 1); - } - // Remove right whitespace - if (j + 1 < tokens.length && tokens[j + 1].kind === TokenKind.Whitespace) { - indicesToRemove.push(j + 1); - } - } - - j++; - } - // Remove the tokens in reverse order to avoid index shifting issues - for (const index of indicesToRemove.sort((a, b) => b - a)) { - tokens.splice(index, 1); - } - } - } - //space to left of function parens? { let parenToken: Token | undefined; @@ -390,6 +334,8 @@ export class InteriorWhitespaceFormatter { tokens = this.formatSpaceBetweenAssociativeArrayLiteralKeyAndColon(tokens, parser, options); + tokens = this.formatSpaceAroundParameterAssignment(tokens, parser, options); + return tokens; } @@ -430,6 +376,34 @@ export class InteriorWhitespaceFormatter { return tokens; } + /** + * Remove spaces around parameter assignments when insertSpaceAroundParameterAssignment is false + */ + private formatSpaceAroundParameterAssignment(tokens: Token[], parser: Parser, options: FormattingOptions) { + if (options.insertSpaceAroundParameterAssignment === true) { + return tokens; + } + + const functionExpressions = [] as FunctionParameterExpression[]; + parser.ast.walk(createVisitor({ + FunctionParameterExpression: (expression) => { + functionExpressions.push(expression); + } + }), { + walkMode: WalkMode.visitAllRecursive + }); + + for (let param of functionExpressions) { + const equalToken = tokens[tokens.indexOf(param.name) + 2]; + if (equalToken && equalToken.kind === TokenKind.Equal) { + this.removeWhitespace(tokens, tokens.indexOf(equalToken) - 1); + this.removeWhitespace(tokens, tokens.indexOf(equalToken) + 1); + } + } + + return tokens; + } + /** * Remove Whitespace tokens backwards until a non-Whitespace token is encountered * @param startIndex the index of the non-Whitespace token to start with. This function will start iterating at `startIndex - 1` From 5245606358e8d62d51d8d6c09f644a509e1c3cdb Mon Sep 17 00:00:00 2001 From: Rob Reed Date: Fri, 28 Nov 2025 14:13:29 -0800 Subject: [PATCH 2/2] Add specificKeywordCaseOverride and specificTypeCaseOverride --- README.md | 33 +++++++++++- bsfmt.schema.json | 10 +++- src/FormattingOptions.ts | 12 +++++ src/formatters/KeywordCaseFormatter.spec.ts | 59 +++++++++++++++++++++ src/formatters/KeywordCaseFormatter.ts | 19 ++++++- 5 files changed, 129 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 127f08d..42f052f 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ bsfmt "source/**/*.brs" "!**/roku_modules/*.*" | noBsfmt |`boolean` | `false` | Don't read a bsfmt.json file | | bsfmtPath |`string` | `undefined` | Use a specified path to bsfmt.json instead of the default | |||| -All boolean, string, and integer [`bsfmt.json`](#bsfmtjson-options) options are supported as well. Complex options such as `keywordCaseOverride` or `typeCaseOverride` are not currently supported via the CLI and should be provided in the `bsfmt.json` or through the node API. Feel free to [open an issue](https://github.com/rokucommunity/brighterscript-formatter/issues/new) if you would like to see support for these options via the CLI. +All boolean, string, and integer [`bsfmt.json`](#bsfmtjson-options) options are supported as well. Complex options such as `keywordCaseOverride`, `typeCaseOverride`, `specificKeywordCaseOverride`, or `specificTypeCaseOverride` are not currently supported via the CLI and should be provided in the `bsfmt.json` or through the node API. Feel free to [open an issue](https://github.com/rokucommunity/brighterscript-formatter/issues/new) if you would like to see support for these options via the CLI. @@ -83,7 +83,9 @@ All boolean, string, and integer [`bsfmt.json`](#bsfmtjson-options) options are |compositeKeywords| `"split", "combine", "original"`| `"split"` | Forces all composite keywords (i.e. `elseif`, `endwhile`, etc...) to be consistent. If `"split"`, they are split into their alternatives (`else if`, `end while`). If `"combine"`', they are combined (`elseif`, `endwhile`). If `"original"` or falsey, they are not modified. | |removeTrailingWhiteSpace|`boolean`|`true`| Remove (or don't remove) trailing whitespace at the end of each line | |[keywordCaseOverride](#keywordCaseOverride)| `object`| `undefined`| Provides a way to override keyword case at the individual TokenType level| -|[typeCaseOverride](#typeCaseOverride)|`object`|`undefined`| Provides a way to override type keyword case at the individual TokenType level.Types are defined as keywords that are preceded by an `as` token.| +|[typeCaseOverride](#typeCaseOverride)|`object`|`undefined`| Provides a way to override type keyword case at the individual TokenType level. Types are defined as keywords that are preceded by an `as` token.| +|[specificKeywordCaseOverride](#specificKeywordCaseOverride)| `object`| `undefined`| Provides a way to override keyword case with a specific string at the individual keyword level| +|[specificTypeCaseOverride](#specificTypeCaseOverride)|`object`|`undefined`| Provides a way to override type keyword case with a specific string at the individual keyword level. Types are defined as keywords that are preceded by an `as` token. This accepts specific casing strings rather than case options like "upper" or "lower".| |formatInteriorWhitespace|`boolean`|`true`| All whitespace between items is reduced to exactly 1 space character and certain keywords and operators are padded with whitespace. This is a catchall property that will also disable the following rules: `insertSpaceBeforeFunctionParenthesis`, `insertSpaceBetweenEmptyCurlyBraces`, `insertSpaceAroundParameterAssignment`, `insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces`| |insertSpaceBeforeFunctionParenthesis|`boolean`|`false`| If true, a space is inserted to the left of an opening function declaration parenthesis. (i.e. `function main ()` or `function ()`). If false, all spacing is removed (i.e. `function main()` or `function()`).| |insertSpaceBetweenEmptyCurlyBraces|`boolean`|`false`| If true, empty curly braces will contain exactly 1 whitespace char (i.e. `{ }`). If false, there will be zero whitespace chars between empty curly braces (i.e. `{}`) | @@ -128,6 +130,33 @@ For more flexibility in how to format the case of types, you can specify the cas A type is any token found directly after an `as` keyword. +### specificKeywordCaseOverride +For more flexibility in how to format the case of keywords, you can specify the case value preference for each individual keyword. Here's an example: + +```js +{ + "specificKeywordCaseOverride": { + "longinteger": "longInteger", + "endif": "EndIF" + } +} +``` + +The full list of keywords detected by this option can be found [here](https://github.com/rokucommunity/brighterscript-formatter/blob/095f9dc5ec418d46d3ea6197712f5d11f71d922f/src/Formatter.ts#L1145). + +### specificTypeCaseOverride +For more flexibility in how to format the case of types, you can specify the case value preference for each individual type. Here's an example: + +```js +{ + "specificTypeCaseOverride": { + "longinteger": "LongInteger" + } +} +``` + +A type is any token found directly after an `as` keyword. + ## Library ### General usage diff --git a/bsfmt.schema.json b/bsfmt.schema.json index 33cbc61..acc2b6c 100644 --- a/bsfmt.schema.json +++ b/bsfmt.schema.json @@ -58,9 +58,17 @@ "default": true, "description": "Removes all trailing whitespace at the end of each line." }, + "specificKeywordCaseOverride": { + "type": "object", + "description": "Allows overriding specific keywords at the individual keyword level.\nExample {\"longinteger\": \"LongInteger\"} would make longinteger LongInteger" + }, + "specificTypeCaseOverride": { + "type": "object", + "description": "Allows overriding specific types at the individual type level.\nExample {\"longinteger\": \"longInteger\"} would make longinteger longInteger" + }, "keywordCaseOverride": { "type": "object", - "description": "Allows overriding case at the individual keyword level.\nExample {\"string\": \"title\"} would make string always lower case regardless of keywordCase", + "description": "Allows overriding case at the individual keyword level.\nExample {\"string\": \"title\"} would make string always title case regardless of keywordCase", "properties": { "endfunction": { "type": "string", diff --git a/src/FormattingOptions.ts b/src/FormattingOptions.ts index 9ff331e..aa6a8ed 100644 --- a/src/FormattingOptions.ts +++ b/src/FormattingOptions.ts @@ -51,6 +51,18 @@ export interface FormattingOptions { * Types are defined as keywords that are preceded by an `as` token. */ typeCaseOverride?: Record; + /** + * Provides a way to override keyword case with a specific string. + * The key should be the lowercased keyword, and the value should be the desired casing. + * e.g. { "longinteger": "LongInteger" } + */ + specificKeywordCaseOverride?: Record; + /** + * Provides a way to override type keyword case with a specific string. + * The key should be the lowercased keyword, and the value should be the desired casing. + * e.g. { "longinteger": "LongInteger" } + */ + specificTypeCaseOverride?: Record; /** * If true (the default), all whitespace between items are reduced to exactly 1 space character, * and certain keywords and operators are padded with whitespace (i.e. `1+1` becomes `1 + 1`). diff --git a/src/formatters/KeywordCaseFormatter.spec.ts b/src/formatters/KeywordCaseFormatter.spec.ts index b85d4e7..a6e10af 100644 --- a/src/formatters/KeywordCaseFormatter.spec.ts +++ b/src/formatters/KeywordCaseFormatter.spec.ts @@ -1,5 +1,7 @@ import { expect } from 'chai'; import { KeywordCaseFormatter } from './KeywordCaseFormatter'; +import { Formatter as MainFormatter } from '../Formatter'; +import { undent } from 'undent'; describe('KeywordCaseFormatter', () => { let Formatter: KeywordCaseFormatter; @@ -22,4 +24,61 @@ describe('KeywordCaseFormatter', () => { expect(Formatter['upperCaseLetter']('hello', 5)).to.equal('hello'); }); }); + + describe('specificCaseOverride', () => { + let formatter: MainFormatter; + beforeEach(() => { + formatter = new MainFormatter(); + }); + + it('specificKeywordCaseOverride', () => { + const input = undent`sub Main() print "hello" end sub`; + const expected = undent`sub Main() PRINT "hello" endSub`; + expect(formatter.format(input, { + keywordCase: 'lower', + specificKeywordCaseOverride: { + endsub: 'endSub', + print: 'PRINT' + } + })).to.equal(expected); + }); + + it('specificTypeCaseOverride', () => { + const input = undent`sub Main(a as integer)`; + const expected = undent`sub Main(a as Integer)`; + expect(formatter.format(input, { + typeCase: 'lower', + specificTypeCaseOverride: { + integer: 'Integer' + } + })).to.equal(expected); + }); + + it('specificKeywordCaseOverride and specificTypeCaseOverride using different cases', () => { + const input = undent`sub longinteger(a as longinteger)`; + const expected = undent`sub LongInteger(a as longInteger)`; + expect(formatter.format(input, { + typeCase: 'lower', + specificKeywordCaseOverride: { + longinteger: 'LongInteger' + }, + specificTypeCaseOverride: { + longinteger: 'longInteger' + } + })).to.equal(expected); + }); + + it('specificKeywordCaseOverride and specificTypeCaseOverride using invalid values (must match the original keyword)', () => { + const input = undent`sub longinteger(a as longinteger)`; + expect(formatter.format(input, { + typeCase: 'lower', + specificKeywordCaseOverride: { + longinteger: 'integer' + }, + specificTypeCaseOverride: { + longinteger: 'integer' + } + })).to.equal(input); + }); + }); }); diff --git a/src/formatters/KeywordCaseFormatter.ts b/src/formatters/KeywordCaseFormatter.ts index d13f6b3..996e72e 100644 --- a/src/formatters/KeywordCaseFormatter.ts +++ b/src/formatters/KeywordCaseFormatter.ts @@ -12,20 +12,37 @@ export class KeywordCaseFormatter { //if this token is a keyword if (Keywords.includes(token.kind)) { - let keywordCase: FormattingOptions['keywordCase']; let lowerKind = token.kind.toLowerCase(); //a token is a type if it's preceded by an `as` token if (this.isType(tokens, token)) { + + //if the token is a type, check for a specific override + const specificTypeCaseOverride = options.specificTypeCaseOverride?.[lowerKind]; + if (specificTypeCaseOverride && specificTypeCaseOverride.toLowerCase() === lowerKind) { + token.text = specificTypeCaseOverride; + continue; + } + //options.typeCase is always set to options.keywordCase when not provided keywordCase = options.typeCase; + //if this is an overridden type keyword, use that override instead if (options.typeCaseOverride && options.typeCaseOverride[lowerKind] !== undefined) { keywordCase = options.typeCaseOverride[lowerKind]; } } else { + //if the token is a keyword, check for a specific override + const specificKeywordCaseOverride = options.specificKeywordCaseOverride?.[lowerKind]; + if (specificKeywordCaseOverride && specificKeywordCaseOverride.toLowerCase() === lowerKind) { + token.text = specificKeywordCaseOverride; + continue; + } + + //keywordCase is always set to options.keywordCase when not provided keywordCase = options.keywordCase; + //if this is an overridable keyword, use that override instead if (options.keywordCaseOverride && options.keywordCaseOverride[lowerKind] !== undefined) { keywordCase = options.keywordCaseOverride[lowerKind];