-
-
Notifications
You must be signed in to change notification settings - Fork 364
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Sindre Sorhus <[email protected]>
- Loading branch information
1 parent
3e2a4e2
commit 5f4c440
Showing
9 changed files
with
933 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
# Fix whitespace-insensitive template indentation | ||
|
||
[Tagged templates](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) often look ugly/jarring because their indentation doesn't match the code they're found in. In many cases, whitespace is insignificant, or a library like [strip-indent](https://www.npmjs.com/package/strip-indent) is used to remove the margin. See [proposal-string-dedent](https://github.com/tc39/proposal-string-dedent) (stage 1 at the time of writing) for a proposal on fixing this in JavaScript. | ||
|
||
This rule will automatically fix the indentation of multiline string templates, to keep them in alignment with the code they are found in. A configurable whitelist is used to ensure no whitespace-sensitive strings are edited. | ||
|
||
## Fail | ||
|
||
```js | ||
function foo() { | ||
const sqlQuery = sql` | ||
select * | ||
from students | ||
where first_name = ${x} | ||
and last_name = ${y} | ||
`; | ||
|
||
const gqlQuery = gql` | ||
query user(id: 5) { | ||
firstName | ||
lastName | ||
} | ||
`; | ||
|
||
const html = /* HTML */ ` | ||
<div> | ||
<span>hello</span> | ||
</div> | ||
`; | ||
} | ||
``` | ||
|
||
## Pass | ||
|
||
The above will auto-fix to: | ||
|
||
```js | ||
function foo() { | ||
const sqlQuery = sql` | ||
select * | ||
from students | ||
where first_name = ${x} | ||
and last_name = ${y} | ||
`; | ||
|
||
const gqlQuery = gql` | ||
query user(id: 5) { | ||
firstName | ||
lastName | ||
} | ||
`; | ||
|
||
const html = /* HTML */ ` | ||
<div> | ||
<span>hello</span> | ||
</div> | ||
`; | ||
} | ||
``` | ||
|
||
Under the hood, [strip-indent](https://npmjs.com/package/strip-indent) is used to determine how the template "should" look. Then a common indent is added to each line based on the margin of the line the template started at. This rule will *not* alter the relative whitespace between significant lines, it will only shift the content right or left so that it aligns sensibly with the surrounding code. | ||
|
||
## Options | ||
|
||
The rule accepts lists of `tags`, `functions`, `selectors` and `comments` to match template literals. `tags` are tagged template literal identifiers, functions are names of utility functions like `stripIndent`, selectors can be any [ESLint selector](https://eslint.org/docs/developer-guide/selectors), and comments are `/* block-commented */` strings. | ||
|
||
Default configuration: | ||
|
||
```js | ||
{ | ||
'unicorn/template-indent': [ | ||
'warn', | ||
{ | ||
tags: [ | ||
'outdent', | ||
'dedent', | ||
'gql', | ||
'sql', | ||
'html', | ||
'styled' | ||
], | ||
functions: [ | ||
'dedent', | ||
'stripIndent' | ||
], | ||
selectors: [], | ||
comments: [ | ||
'HTML', | ||
'indent' | ||
] | ||
} | ||
] | ||
} | ||
``` | ||
|
||
You can use a selector for custom use-cases, like indenting *all* template literals, even those without template tags or function callers: | ||
|
||
```js | ||
{ | ||
'unicorn/template-indent': [ | ||
'warn', | ||
{ | ||
tags: [], | ||
functions: [], | ||
selectors: [ | ||
'TemplateLiteral' | ||
] | ||
} | ||
] | ||
} | ||
``` | ||
|
||
Indentation will be done with tabs or spaces depending on the line of code that the template literal starts at. You can override this by supplying an `indent`, which should be either a number (of spaces) or a string consisting only of whitespace characters: | ||
|
||
```js | ||
{ | ||
'unicorn/template-indent': [ | ||
'warn', { | ||
indent: 8, | ||
} | ||
] | ||
} | ||
``` | ||
|
||
```js | ||
{ | ||
'unicorn/template-indent': [ | ||
'warn', | ||
{ | ||
indent: '\t\t' | ||
} | ||
] | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
'use strict'; | ||
const stripIndent = require('strip-indent'); | ||
const indentString = require('indent-string'); | ||
const esquery = require('esquery'); | ||
const {replaceTemplateElement} = require('./fix/index.js'); | ||
|
||
const MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE = 'template-indent'; | ||
const messages = { | ||
[MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE]: 'Templates should be properly indented.', | ||
}; | ||
|
||
/** @param {import('eslint').Rule.RuleContext} context */ | ||
const create = context => { | ||
const sourceCode = context.getSourceCode(); | ||
const options = { | ||
tags: ['outdent', 'dedent', 'gql', 'sql', 'html', 'styled'], | ||
functions: ['dedent', 'stripIndent'], | ||
selectors: [], | ||
comments: ['HTML', 'indent'], | ||
...context.options[0], | ||
}; | ||
|
||
options.comments = options.comments.map(comment => comment.toLowerCase()); | ||
|
||
const selectors = [ | ||
...options.tags.map(tag => `TaggedTemplateExpression[tag.name="${tag}"] > .quasi`), | ||
...options.functions.map(fn => `CallExpression[callee.name="${fn}"] > .arguments`), | ||
...options.selectors, | ||
]; | ||
|
||
/** @param {import('@babel/core').types.TemplateLiteral} node */ | ||
const indentTemplateLiteralNode = node => { | ||
const delimiter = '__PLACEHOLDER__' + Math.random(); | ||
const joined = node.quasis | ||
.map(quasi => { | ||
const untrimmedText = sourceCode.getText(quasi); | ||
return untrimmedText.slice(1, quasi.tail ? -1 : -2); | ||
}) | ||
.join(delimiter); | ||
|
||
const eolMatch = joined.match(/\r?\n/); | ||
if (!eolMatch) { | ||
return; | ||
} | ||
|
||
const eol = eolMatch[0]; | ||
|
||
const startLine = sourceCode.lines[node.loc.start.line - 1]; | ||
const marginMatch = startLine.match(/^(\s*)\S/); | ||
const parentMargin = marginMatch ? marginMatch[1] : ''; | ||
|
||
let indent; | ||
if (typeof options.indent === 'string') { | ||
indent = options.indent; | ||
} else if (typeof options.indent === 'number') { | ||
indent = ' '.repeat(options.indent); | ||
} else { | ||
const tabs = parentMargin.startsWith('\t'); | ||
indent = tabs ? '\t' : ' '; | ||
} | ||
|
||
const dedented = stripIndent(joined); | ||
const fixed | ||
= eol | ||
+ indentString(dedented.trim(), 1, {indent: parentMargin + indent}) | ||
+ eol | ||
+ parentMargin; | ||
|
||
if (fixed === joined) { | ||
return; | ||
} | ||
|
||
context.report({ | ||
node, | ||
messageId: MESSAGE_ID_IMPROPERLY_INDENTED_TEMPLATE, | ||
fix: fixer => fixed | ||
.split(delimiter) | ||
.map((replacement, index) => replaceTemplateElement(fixer, node.quasis[index], replacement)), | ||
}); | ||
}; | ||
|
||
return { | ||
/** @param {import('@babel/core').types.TemplateLiteral} node */ | ||
TemplateLiteral: node => { | ||
if (options.comments.length > 0) { | ||
const previousToken = sourceCode.getTokenBefore(node, {includeComments: true}); | ||
if (previousToken && previousToken.type === 'Block' && options.comments.includes(previousToken.value.trim().toLowerCase())) { | ||
indentTemplateLiteralNode(node); | ||
return; | ||
} | ||
} | ||
|
||
const ancestry = context.getAncestors().reverse(); | ||
const shouldIndent = selectors.some(selector => esquery.matches(node, esquery.parse(selector), ancestry)); | ||
|
||
if (shouldIndent) { | ||
indentTemplateLiteralNode(node); | ||
} | ||
}, | ||
}; | ||
}; | ||
|
||
/** @type {import('json-schema').JSONSchema7[]} */ | ||
const schema = [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
indent: { | ||
oneOf: [ | ||
{ | ||
type: 'string', | ||
pattern: /^\s+$/.source, | ||
}, | ||
{ | ||
type: 'integer', | ||
minimum: 1, | ||
}, | ||
], | ||
}, | ||
tags: { | ||
type: 'array', | ||
uniqueItems: true, | ||
items: { | ||
type: 'string', | ||
}, | ||
}, | ||
functions: { | ||
type: 'array', | ||
uniqueItems: true, | ||
items: { | ||
type: 'string', | ||
}, | ||
}, | ||
selectors: { | ||
type: 'array', | ||
uniqueItems: true, | ||
items: { | ||
type: 'string', | ||
}, | ||
}, | ||
comments: { | ||
type: 'array', | ||
uniqueItems: true, | ||
items: { | ||
type: 'string', | ||
}, | ||
}, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
]; | ||
|
||
module.exports = { | ||
create, | ||
meta: { | ||
type: 'suggestion', | ||
docs: { | ||
description: 'Fix whitespace-insensitive template indentation.', | ||
}, | ||
fixable: 'code', | ||
schema, | ||
messages, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.