-
-
Notifications
You must be signed in to change notification settings - Fork 90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
padded-blocks
: Add "start" & "end" options
#308
Comments
Just an initial sketch of an idea .... feel free to edit Updated code.../**
* @fileoverview A rule to ensure blank lines within blocks.
* @author Mathias Schreck <https://github.com/lo1tuma>
*/
import type { ASTNode, Token, Tree } from '@shared/types'
import { isTokenOnSameLine } from '../../utils/ast-utils'
import { createRule } from '../../utils/createRule'
import type { MessageIds, RuleOptions } from './types'
export default createRule<MessageIds, RuleOptions>({
meta: {
type: 'layout',
docs: {
description: 'Require or disallow padding within blocks',
url: 'https://eslint.style/rules/js/padded-blocks',
},
fixable: 'whitespace',
schema: [
{
oneOf: [
{
type: 'string',
enum: ['always', 'never', 'start', 'end'],
},
{
type: 'object',
properties: {
blocks: {
type: 'string',
enum: ['always', 'never', 'start', 'end'],
},
switches: {
type: 'string',
enum: ['always', 'never', 'start', 'end'],
},
classes: {
type: 'string',
enum: ['always', 'never', 'start', 'end'],
},
},
additionalProperties: false,
minProperties: 1,
},
],
},
{
type: 'object',
properties: {
allowSingleLineBlocks: {
type: 'boolean',
},
},
additionalProperties: false,
},
],
messages: {
// alwaysPadBlock: 'Block must be padded by blank lines.',
// neverPadBlock: 'Block must not be padded by blank lines.',
startPadBlock: 'Block must be padded by a starting blank line.',
endPadBlock: 'Block must be padded by an ending blank line.',
startNoPadBlock: 'Block must not be padded by a starting blank line.',
endNoPadBlock: 'Block must not be padded by an ending blank line.',
},
},
create(context) {
const options: Record<string, string | boolean> = {}
// context possibilities: {'string'} | {'string', {...}} | {{...}}
const defaultOption = typeof context.options[0] === 'string' ? context.options[0] : 'always'
const exceptOptions = context.options[1] ? context.options[1] : typeof context.options[0] === 'object' ? context.options[0] : {}
// note: exceptOptions values for blocks|switches|classes must be string (not boolean)
options.blocks = exceptOptions.blocks || defaultOption
options.switches = exceptOptions.switches || defaultOption
options.classes = exceptOptions.classes || defaultOption
options.allowSingleLineBlocks = exceptOptions.allowSingleLineBlocks === true
const sourceCode = context.sourceCode
/**
* Gets the open brace token from a given node.
* @param node A BlockStatement or SwitchStatement node from which to get the open brace.
* @returns The token of the open brace.
*/
function getOpenBrace(node: Tree.BlockStatement | Tree.StaticBlock | Tree.SwitchStatement | Tree.ClassBody): Token {
if (node.type === 'SwitchStatement')
return sourceCode.getTokenBefore(node.cases[0])!
if (node.type === 'StaticBlock')
return sourceCode.getFirstToken(node, { skip: 1 })! // skip the `static` token
// `BlockStatement` or `ClassBody`
return sourceCode.getFirstToken(node)!
}
/**
* Checks if the given parameter is a comment node
* @param node An AST node or token
* @returns True if node is a comment
*/
function isComment(node: ASTNode | Token) {
return node.type === 'Line' || node.type === 'Block'
}
/**
* Checks if there is padding between two tokens
* @param first The first token
* @param second The second token
* @returns True if there is at least a line between the tokens
*/
function isPaddingBetweenTokens(first: Token, second: Token) {
return second.loc.start.line - first.loc.end.line >= 2
}
/**
* Checks if the given token has a blank line after it.
* @param token The token to check.
* @returns Whether or not the token is followed by a blank line.
*/
function getFirstBlockToken(token: Token) {
let prev
let first = token
do {
prev = first
first = sourceCode.getTokenAfter(first, { includeComments: true })!
} while (isComment(first) && first.loc.start.line === prev.loc.end.line)
return first
}
/**
* Checks if the given token is preceded by a blank line.
* @param token The token to check
* @returns Whether or not the token is preceded by a blank line
*/
function getLastBlockToken(token: Token) {
let last = token
let next
do {
next = last
last = sourceCode.getTokenBefore(last, { includeComments: true })!
} while (isComment(last) && last.loc.end.line === next.loc.start.line)
return last
}
/**
* Checks if a node should be padded, according to the rule config.
* @param node The AST node to check.
* @throws {Error} (Unreachable)
* @returns True if the node should be padded, false otherwise.
*/
function requirePaddingFor(node: ASTNode) {
switch (node.type) {
case 'BlockStatement':
case 'StaticBlock':
return options.blocks
case 'SwitchStatement':
return options.switches
case 'ClassBody':
return options.classes
/* c8 ignore next */
default:
throw new Error('unreachable')
}
}
/**
* Checks the given BlockStatement node to be padded if the block is not empty.
* @param node The AST node of a BlockStatement.
*/
function checkPadding(node: Tree.BlockStatement | Tree.SwitchStatement | Tree.ClassBody) {
const openBrace = getOpenBrace(node)
const firstBlockToken = getFirstBlockToken(openBrace)
const tokenBeforeFirst = sourceCode.getTokenBefore(firstBlockToken, { includeComments: true })!
const closeBrace = sourceCode.getLastToken(node)!
const lastBlockToken = getLastBlockToken(closeBrace)
const tokenAfterLast = sourceCode.getTokenAfter(lastBlockToken, { includeComments: true })!
const blockHasTopPadding = isPaddingBetweenTokens(tokenBeforeFirst, firstBlockToken)
const blockHasBottomPadding = isPaddingBetweenTokens(lastBlockToken, tokenAfterLast)
if (options.allowSingleLineBlocks && isTokenOnSameLine(tokenBeforeFirst, tokenAfterLast))
return
// default report
const report = {
node,
loc: {
end: tokenAfterLast.loc.start,
start: lastBlockToken.loc.end,
},
}
// return value undefined | always | never| start | end
switch (requirePaddingFor(node)) {
case 'always':
!blockHasTopPadding && addStartPad(report, tokenBeforeFirst)
!blockHasBottomPadding && addEndPad(report, tokenAfterLast)
break;
case 'never':
blockHasTopPadding && removeStartPad(report, tokenBeforeFirst, firstBlockToken)
blockHasBottomPadding && removeEndPad(report, lastBlockToken, tokenAfterLast)
break;
case 'start':
!blockHasTopPadding && addStartPad(report, tokenBeforeFirst)
break;
case 'end':
!blockHasBottomPadding && addEndPad(report, tokenAfterLast)
break;
}
}
function addStartPad(report, tokenBeforeFirst) {
report.fix = (fixer) => fixer.insertTextAfter(tokenBeforeFirst, '\n')
report.messageId = 'startPadBlock'
context.report(report)
}
function addEndPad(report, tokenAfterLast) {
report.fix = (fixer) => fixer.insertTextBefore(tokenAfterLast, '\n')
report.messageId = 'endPadBlock'
context.report(report)
}
function removeStartPad(report, tokenBeforeFirst, firstBlockToken) {
report.fix = (fixer) => fixer.replaceTextRange([tokenBeforeFirst.range[1], firstBlockToken.range[0] - firstBlockToken.loc.start.column], '\n')
report.messageId = 'startNoPadBlock'
context.report(report)
}
function removeEndPad(report, lastBlockToken, tokenAfterLast) {
report.fix = (fixer) => fixer.replaceTextRange([lastBlockToken.range[1], tokenAfterLast.range[0] - tokenAfterLast.loc.start.column], '\n')
report.messageId = 'endNoPadBlock'
context.report(report)
}
const rule: Record<string, any> = {}
if (Object.prototype.hasOwnProperty.call(options, 'switches')) {
rule.SwitchStatement = function (node: Tree.SwitchStatement) {
if (node.cases.length === 0)
return
checkPadding(node)
}
}
if (Object.prototype.hasOwnProperty.call(options, 'blocks')) {
rule.BlockStatement = function (node: Tree.BlockStatement) {
if (node.body.length === 0)
return
checkPadding(node)
}
rule.StaticBlock = rule.BlockStatement
}
if (Object.prototype.hasOwnProperty.call(options, 'classes')) {
rule.ClassBody = function (node: Tree.ClassBody) {
if (node.body.length === 0)
return
checkPadding(node)
}
}
return rule
},
}) |
Sounds good to me, PR welcome! |
Updated code in #308 (comment) |
I'm willing to make a PR for this. Before that, I want to confirm if it's more appropriate to use |
@antfu any suggestions? |
Clear and concise description of the problem
Add "start" & "end" options
It would be useful to have more control over the "padded-blocks".
(I write classes/switches with padded start only.)
Suggested solution
Add "start" & "end" options to have more control over padding blocks
The rule already differentiates between the start & end padding i.e.
eslint-stylistic/packages/eslint-plugin-js/rules/padded-blocks/padded-blocks.ts
Lines 193 to 194 in beef00f
Alternative
No response
Additional context
No response
Validations
Contributes
The text was updated successfully, but these errors were encountered: