📄 plugin-test-formatting.ts
¶
📊 Analysis Summary¶
Metric | Count |
---|---|
🔧 Functions | 15 |
📦 Imports | 6 |
📊 Variables & Constants | 21 |
📑 Type Aliases | 3 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin-internal/src/rules/plugin-test-formatting.ts
📦 Imports¶
Name | Source |
---|---|
TSESTree |
@typescript-eslint/utils |
prettier |
@prettier/sync |
getContextualType |
@typescript-eslint/type-utils |
AST_NODE_TYPES |
@typescript-eslint/utils |
ESLintUtils |
@typescript-eslint/utils |
createRule |
../util |
Variables & Constants¶
Name | Type | Kind | Value | Exported |
---|---|---|---|---|
prettierConfig |
any |
const | prettier.resolveConfig(__dirname) ?? {} |
✗ |
START_OF_LINE_WHITESPACE_MATCHER |
RegExp |
const | /^( *)/ |
✗ |
BACKTICK_REGEX |
RegExp |
const | / /g` |
✗ |
TEMPLATE_EXPR_OPENER |
RegExp |
const | /\$\{/g |
✗ |
lineIdx |
number |
const | node.loc.start.line - 1 |
✗ |
indent |
string |
const | `START_OF_LINE_WHITESPACE_MATCHER.exec( | |
sourceCodeLines[lineIdx], | ||||
)![1]` | ✗ | |||
fixed |
string |
let/var | code |
✗ |
checkedObjects |
Set<TSESTree.ObjectExpression> |
const | new Set<TSESTree.ObjectExpression>() |
✗ |
message |
string |
let/var | formatted.message |
✗ |
quote |
string |
const | quoteIn ?? getQuote(output) |
✗ |
text |
any |
const | literal.quasis[0].value.cooked |
✗ |
lastLine |
any |
const | lines[lines.length - 1] |
✗ |
isStartEmpty |
boolean |
const | lines[0].trimEnd() === '' |
✗ |
isEndEmpty |
boolean |
const | lastLine.trimStart() === '' |
✗ |
expectedIndent |
number |
const | parentIndent + 2 |
✗ |
firstLineIndent |
string |
const | `START_OF_LINE_WHITESPACE_MATCHER.exec( | |
lines[0], | ||||
)![1]` | ✗ | |||
requiresIndent |
boolean |
const | firstLineIndent.length > 0 |
✗ |
matches |
RegExpExecArray |
const | START_OF_LINE_WHITESPACE_MATCHER.exec(line)! |
✗ |
indent |
string |
const | matches[1] |
✗ |
formattedIndented |
string |
const | `requiresIndent | |
? formatted | ||||
.split('\n') | ||||
.map(l => doIndent(l, expectedIndent)) | ||||
.join('\n') | ||||
: formatted` | ✗ | |||
memberExpr |
any |
const | callExpr.callee |
✗ |
Functions¶
getExpectedIndentForNode(node: TSESTree.Node, sourceCodeLines: string[]): number
¶
Code
function getExpectedIndentForNode(
node: TSESTree.Node,
sourceCodeLines: string[],
): number {
const lineIdx = node.loc.start.line - 1;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const indent = START_OF_LINE_WHITESPACE_MATCHER.exec(
sourceCodeLines[lineIdx],
)![1];
return indent.length;
}
- Parameters:
node: TSESTree.Node
sourceCodeLines: string[]
- Return Type:
number
- Calls:
START_OF_LINE_WHITESPACE_MATCHER.exec
- Internal Comments:
doIndent(line: string, indent: number): string
¶
Code
- Parameters:
line: string
indent: number
- Return Type:
string
getQuote(code: string): "'" | '"' | null
¶
Code
- Parameters:
code: string
- Return Type:
"'" | '"' | null
- Calls:
code.includes
- Internal Comments:
escapeTemplateString(code: string): string
¶
Code
- Parameters:
code: string
- Return Type:
string
- Calls:
fixed.replaceAll
getCodeFormatted(code: string): string | FormattingError
¶
Code
function getCodeFormatted(code: string): string | FormattingError {
try {
return prettier
.format(code, {
...prettierConfig,
parser: 'typescript',
})
.trimEnd(); // prettier will insert a new line at the end of the code
} catch (ex) {
// ex instanceof Error is false as of @prettier/sync@0.3.0, as is ex instanceof SyntaxError
if (
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(ex as Partial<Error> | undefined)?.constructor?.name !==
'SyntaxError'
) {
throw ex;
}
return ex as FormattingError;
}
}
- Parameters:
code: string
- Return Type:
string | FormattingError
- Calls:
prettier .format(code, { ...prettierConfig, parser: 'typescript', }) .trimEnd
- Internal Comments:
getCodeFormattedOrReport(code: string, location: TSESTree.Node): string | null
¶
Code
function getCodeFormattedOrReport(
code: string,
location: TSESTree.Node,
): string | null {
if (formatWithPrettier === false) {
return null;
}
const formatted = getCodeFormatted(code);
if (typeof formatted === 'string') {
return formatted;
}
let message = formatted.message;
if (formatted.codeFrame) {
message = message.replace(`\n${formatted.codeFrame}`, '');
}
if (formatted.loc) {
message = message.replace(/ \(\d+:\d+\)$/, '');
}
context.report({
node: location,
messageId: 'prettierException',
data: {
message,
},
});
return null;
}
- Parameters:
code: string
location: TSESTree.Node
- Return Type:
string | null
- Calls:
getCodeFormatted
message.replace
context.report
checkExpression(node: TSESTree.Node | null, isErrorTest: boolean): void
¶
Code
function checkExpression(
node: TSESTree.Node | null,
isErrorTest: boolean,
): void {
switch (node?.type) {
case AST_NODE_TYPES.Literal:
checkLiteral(node, isErrorTest);
break;
case AST_NODE_TYPES.TemplateLiteral:
checkTemplateLiteral(node, isErrorTest);
break;
case AST_NODE_TYPES.TaggedTemplateExpression:
checkTaggedTemplateExpression(node, isErrorTest);
break;
case AST_NODE_TYPES.CallExpression:
checkCallExpression(node, isErrorTest);
break;
}
}
- Parameters:
node: TSESTree.Node | null
isErrorTest: boolean
- Return Type:
void
- Calls:
checkLiteral
checkTemplateLiteral
checkTaggedTemplateExpression
checkCallExpression
checkLiteral(literal: TSESTree.Literal, isErrorTest: boolean, quoteIn: string): void
¶
Code
function checkLiteral(
literal: TSESTree.Literal,
isErrorTest: boolean,
quoteIn?: string,
): void {
if (typeof literal.value === 'string') {
const output = getCodeFormattedOrReport(literal.value, literal);
if (output && output !== literal.value) {
context.report({
node: literal,
messageId: isErrorTest
? 'invalidFormattingErrorTest'
: 'invalidFormatting',
fix(fixer) {
if (output.includes('\n')) {
// formatted string is multiline, then have to use backticks
return fixer.replaceText(
literal,
`\`${escapeTemplateString(output)}\``,
);
}
const quote = quoteIn ?? getQuote(output);
if (quote == null) {
return null;
}
return fixer.replaceText(literal, `${quote}${output}${quote}`);
},
});
}
}
}
- Parameters:
literal: TSESTree.Literal
isErrorTest: boolean
quoteIn: string
- Return Type:
void
- Calls:
getCodeFormattedOrReport
context.report
output.includes
fixer.replaceText
escapeTemplateString
getQuote
- Internal Comments:
checkTemplateLiteral(literal: TSESTree.TemplateLiteral, isErrorTest: boolean, isNoFormatTagged: boolean): void
¶
Code
function checkTemplateLiteral(
literal: TSESTree.TemplateLiteral,
isErrorTest: boolean,
isNoFormatTagged = false,
): void {
if (literal.quasis.length > 1) {
// ignore template literals with ${expressions} for simplicity
return;
}
const text = literal.quasis[0].value.cooked;
if (literal.loc.end.line === literal.loc.start.line) {
// don't use template strings for single line tests
return context.report({
node: literal,
messageId: 'singleLineQuotes',
fix(fixer) {
const quote = getQuote(text);
if (quote == null) {
return null;
}
return [
fixer.replaceTextRange(
[literal.range[0], literal.range[0] + 1],
quote,
),
fixer.replaceTextRange(
[literal.range[1] - 1, literal.range[1]],
quote,
),
];
},
});
}
const lines = text.split('\n');
const lastLine = lines[lines.length - 1];
// prettier will trim out the end of line on save, but eslint will check before then
const isStartEmpty = lines[0].trimEnd() === '';
// last line can be indented
const isEndEmpty = lastLine.trimStart() === '';
if (!isStartEmpty || !isEndEmpty) {
// multiline template strings must have an empty first/last line
return context.report({
node: literal,
messageId: 'templateLiteralEmptyEnds',
*fix(fixer) {
if (!isStartEmpty) {
yield fixer.replaceTextRange(
[literal.range[0], literal.range[0] + 1],
'`\n',
);
}
if (!isEndEmpty) {
yield fixer.replaceTextRange(
[literal.range[1] - 1, literal.range[1]],
'\n`',
);
}
},
});
}
const parentIndent = getExpectedIndentForNode(
literal,
context.sourceCode.lines,
);
if (lastLine.length !== parentIndent) {
return context.report({
node: literal,
messageId: 'templateLiteralLastLineIndent',
fix(fixer) {
return fixer.replaceTextRange(
[literal.range[1] - lastLine.length - 1, literal.range[1]],
doIndent('`', parentIndent),
);
},
});
}
// remove the empty lines
lines.pop();
lines.shift();
// +2 because we expect the string contents are indented one level
const expectedIndent = parentIndent + 2;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const firstLineIndent = START_OF_LINE_WHITESPACE_MATCHER.exec(
lines[0],
)![1];
const requiresIndent = firstLineIndent.length > 0;
if (requiresIndent) {
if (firstLineIndent.length !== expectedIndent) {
return context.report({
node: literal,
messageId: 'templateStringRequiresIndent',
data: {
indent: expectedIndent,
},
});
}
// quick-and-dirty validation that lines are roughly indented correctly
for (const line of lines) {
if (line.length === 0) {
// empty lines are valid
continue;
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const matches = START_OF_LINE_WHITESPACE_MATCHER.exec(line)!;
const indent = matches[1];
if (indent.length < expectedIndent) {
return context.report({
node: literal,
messageId: 'templateStringMinimumIndent',
data: {
indent: expectedIndent,
},
});
}
}
// trim the lines to remove expectedIndent characters from the start
// this makes it easier to check formatting
for (let i = 0; i < lines.length; i += 1) {
lines[i] = lines[i].substring(expectedIndent);
}
}
const code = lines.join('\n');
if (isNoFormatTagged) {
if (literal.parent.type === AST_NODE_TYPES.TaggedTemplateExpression) {
checkForUnnecesaryNoFormat(code, literal.parent);
}
return;
}
const formatted = getCodeFormattedOrReport(code, literal);
if (formatted && formatted !== code) {
const formattedIndented = requiresIndent
? formatted
.split('\n')
.map(l => doIndent(l, expectedIndent))
.join('\n')
: formatted;
return context.report({
node: literal,
messageId: isErrorTest
? 'invalidFormattingErrorTest'
: 'invalidFormatting',
fix(fixer) {
return fixer.replaceText(
literal,
`\`\n${escapeTemplateString(formattedIndented)}\n${doIndent(
'',
parentIndent,
)}\``,
);
},
});
}
}
- Parameters:
literal: TSESTree.TemplateLiteral
isErrorTest: boolean
isNoFormatTagged: boolean
- Return Type:
void
- Calls:
context.report
getQuote
fixer.replaceTextRange
text.split
lines[0].trimEnd
lastLine.trimStart
getExpectedIndentForNode
doIndent
lines.pop
lines.shift
START_OF_LINE_WHITESPACE_MATCHER.exec
lines[i].substring
lines.join
checkForUnnecesaryNoFormat
getCodeFormattedOrReport
formatted .split('\n') .map(l => doIndent(l, expectedIndent)) .join
fixer.replaceText
escapeTemplateString
- Internal Comments:
// ignore template literals with ${expressions} for simplicity // don't use template strings for single line tests // prettier will trim out the end of line on save, but eslint will check before then (x2) // last line can be indented (x2) // multiline template strings must have an empty first/last line // remove the empty lines (x4) // +2 because we expect the string contents are indented one level (x2) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion (x4) // quick-and-dirty validation that lines are roughly indented correctly // empty lines are valid // trim the lines to remove expectedIndent characters from the start // this makes it easier to check formatting
isNoFormatTemplateTag(tag: TSESTree.Expression): boolean
¶
Code
- Parameters:
tag: TSESTree.Expression
- Return Type:
boolean
checkForUnnecesaryNoFormat(text: string, expr: TSESTree.TaggedTemplateExpression): void
¶
Code
function checkForUnnecesaryNoFormat(
text: string,
expr: TSESTree.TaggedTemplateExpression,
): void {
const formatted = getCodeFormatted(text);
if (formatted === text) {
context.report({
node: expr,
messageId: 'noUnnecessaryNoFormat',
fix(fixer) {
if (expr.loc.start.line === expr.loc.end.line) {
return fixer.replaceText(expr, `'${escapeTemplateString(text)}'`);
}
return fixer.replaceText(expr.tag, '');
},
});
}
}
- Parameters:
text: string
expr: TSESTree.TaggedTemplateExpression
- Return Type:
void
- Calls:
getCodeFormatted
context.report
fixer.replaceText
escapeTemplateString
checkTaggedTemplateExpression(expr: TSESTree.TaggedTemplateExpression, isErrorTest: boolean): void
¶
Code
function checkTaggedTemplateExpression(
expr: TSESTree.TaggedTemplateExpression,
isErrorTest: boolean,
): void {
if (isNoFormatTemplateTag(expr.tag)) {
const { cooked } = expr.quasi.quasis[0].value;
checkForUnnecesaryNoFormat(cooked, expr);
} else {
return;
}
if (expr.loc.start.line === expr.loc.end.line) {
// all we do on single line test cases is check format, but there's no formatting to do
return;
}
checkTemplateLiteral(
expr.quasi,
isErrorTest,
isNoFormatTemplateTag(expr.tag),
);
}
- Parameters:
expr: TSESTree.TaggedTemplateExpression
isErrorTest: boolean
- Return Type:
void
- Calls:
isNoFormatTemplateTag
checkForUnnecesaryNoFormat
checkTemplateLiteral
- Internal Comments:
checkCallExpression(callExpr: TSESTree.CallExpression, isErrorTest: boolean): void
¶
Code
function checkCallExpression(
callExpr: TSESTree.CallExpression,
isErrorTest: boolean,
): void {
if (callExpr.callee.type !== AST_NODE_TYPES.MemberExpression) {
return;
}
const memberExpr = callExpr.callee;
// handle cases like 'aa'.trimRight and `aa`.trimRight()
checkExpression(memberExpr.object, isErrorTest);
}
- Parameters:
callExpr: TSESTree.CallExpression
isErrorTest: boolean
- Return Type:
void
- Calls:
checkExpression
- Internal Comments:
checkInvalidTest(test: TSESTree.ObjectExpression, isErrorTest: boolean): void
¶
Code
function checkInvalidTest(
test: TSESTree.ObjectExpression,
isErrorTest = true,
): void {
if (checkedObjects.has(test)) {
return;
}
checkedObjects.add(test);
for (const prop of test.properties) {
if (
prop.type !== AST_NODE_TYPES.Property ||
prop.computed ||
prop.key.type !== AST_NODE_TYPES.Identifier
) {
continue;
}
if (prop.key.name === 'code') {
checkExpression(prop.value, isErrorTest);
}
}
}
- Parameters:
test: TSESTree.ObjectExpression
isErrorTest: boolean
- Return Type:
void
- Calls:
checkedObjects.has
checkedObjects.add
checkExpression
checkValidTest(tests: TSESTree.ArrayExpression): void
¶
Code
function checkValidTest(tests: TSESTree.ArrayExpression): void {
for (const test of tests.elements) {
switch (test?.type) {
case AST_NODE_TYPES.ObjectExpression:
// delegate object-style tests to the invalid checker
checkInvalidTest(test, false);
break;
default:
checkExpression(test, false);
break;
}
}
}
- Parameters:
tests: TSESTree.ArrayExpression
- Return Type:
void
- Calls:
checkInvalidTest
checkExpression
- Internal Comments:
Type Aliases¶
Options
¶
type Options = [
{
// This option exists so that rules like type-annotation-spacing can exist without every test needing a prettier-ignore
formatWithPrettier?: boolean;
},
];
MessageIds
¶
type MessageIds = | 'invalidFormatting'
| 'invalidFormattingErrorTest'
| 'noUnnecessaryNoFormat'
| 'prettierException'
| 'singleLineQuotes'
| 'templateLiteralEmptyEnds'
| 'templateLiteralLastLineIndent'
| 'templateStringMinimumIndent'
| 'templateStringRequiresIndent';