Skip to content
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

Use official pug-lint to lint pug files #65

Merged
merged 10 commits into from
Jun 14, 2019
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,7 @@ Then configure the rules you want to use under the rules section.
* [`react-pug/quotes`](./docs/rules/quotes.md): Manage quotes in Pug
* [`react-pug/uses-react`](./docs/rules/uses-react.md): Prevent React to be marked as unused
* [`react-pug/uses-vars`](./docs/rules/uses-vars.md): Prevent variables used in Pug to be marked as unused

Experimental:

* [`react-pug/pug-lint`](./docs/rules/pug-lint.md): Inherit pug-lint to validate pug
31 changes: 31 additions & 0 deletions docs/rules/pug-lint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Inherit pug-lint to validate pug (experimental)

This rule applies pug-lint to pug templates in js.

[See pug-lint project](https://github.com/pugjs/pug-lint)

## Rule Details

The following patterns are considered warnings:

```jsx
/*eslint react-pug/pug-lint: ["error", { "requireSpaceAfterCodeOperator": true }]*/
pug`div=greeting`
```

```jsx
/*eslint react-pug/pug-lint: ["error", { "disallowTrailingSpaces": true }]*/
pug`div: img() `
```

The following patterns are **not** considered warnings:

```jsx
/*eslint react-pug/pug-lint: ["error", { "requireSpaceAfterCodeOperator": true }]*/
pug`div= greeting`
```

```jsx
/*eslint react-pug/pug-lint: ["error", { "disallowTrailingSpaces": true }]*/
pug`div: img()`
```
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const allRules = {
'no-broken-template': require('./lib/rules/no-broken-template'),
'no-interpolation': require('./lib/rules/no-interpolation'),
'no-undef': require('./lib/rules/no-undef'),
'pug-lint': require('./lib/rules/pug-lint'),
quotes: require('./lib/rules/quotes'),
'uses-react': require('./lib/rules/uses-react'),
'uses-vars': require('./lib/rules/uses-vars'),
Expand All @@ -29,6 +30,7 @@ module.exports = {
'react-pug/indent': 2,
'react-pug/no-broken-template': 2,
'react-pug/no-undef': 2,
'react-pug/pug-lint': 2,
'react-pug/no-interpolation': 2,
'react-pug/quotes': 2,
'react-pug/uses-react': 2,
Expand Down
118 changes: 118 additions & 0 deletions lib/rules/pug-lint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/**
* @fileoverview Inherit pug-lint to validate pug
* @author Eugene Zhlobo
*/

const Linter = require('pug-lint')
const common = require('common-prefix')

const { isReactPugReference, buildLocation, docsUrl } = require('../util/eslint')
const getTemplate = require('../util/getTemplate')

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

const buildMessage = actual => (
`Invalid indentation, found "${actual}" spaces`
)

module.exports = {
meta: {
docs: {
description: 'Inherit pug-lint to validate pug (experimental)',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('pug-lint'),
},
schema: [
{
type: 'object',
},
],
},

create: function (context) {
return {
TaggedTemplateExpression: function (node) {
if (isReactPugReference(node)) {
const template = getTemplate(node)
const lines = template.split('\n')

const linter = new Linter()

linter.configure(context.options[0])

const firstTokenInLine = context
.getSourceCode()
.getTokensBefore(node, {
filter: token => token.loc.end.line === node.loc.start.line,
})[0]

const minimalIndent = firstTokenInLine
? firstTokenInLine.loc.start.column
: node.loc.start.column

const desiredIndent = lines.length > 1
? minimalIndent + 2
: 0

const amountOfUselessSpaces = common(lines.filter(item => item.trim() !== ''))
.replace(/^(\s*).*/, '$1')
.length

if (amountOfUselessSpaces > 0 && amountOfUselessSpaces < desiredIndent) {
context.report({
node,
message: buildMessage(amountOfUselessSpaces),
loc: buildLocation(
[(node.loc.start.line + 1), 0],
[(node.loc.start.line + 1), amountOfUselessSpaces],
),
})

return null
}

// We need to pass the template without not valuable spaces in the
// beginning of each line
const preparedTemplate = lines
.map(item => item.slice(desiredIndent))
.join('\n')

const result = linter.checkString(preparedTemplate, 'testfile')

if (result.length) {
result.forEach((error) => {
const delta = error.line === 1
// When template starts plus backtick
? node.quasi.quasis[0].loc.start.column + 1
: desiredIndent - 1

let columnStart = error.column + delta
let columnEnd = error.column + delta
let message = error.msg

if (error.msg === 'Invalid indentation') {
columnStart = 0
columnEnd = preparedTemplate.split('\n')[error.line - 1].replace(/^(\s*).*/, '$1').length + desiredIndent
message = buildMessage(columnEnd)
}

context.report({
node,
message,
loc: buildLocation(
[(node.loc.start.line + error.line) - 1, columnStart],
[(node.loc.start.line + error.line) - 1, columnEnd],
),
})
})
}
}

return null
},
}
},
}
19 changes: 19 additions & 0 deletions lib/util/testBuildCases.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const extractCase = type => item => ({
options: item.options || [],
...item[type],
})

const extractCases = type => (items) => {
if (items.some(item => item.only)) {
return items.filter(item => item.only).map(extractCase(type))
}

return items.map(extractCase(type))
}

module.exports = function (cases) {
return {
valid: extractCases('valid')(cases),
invalid: extractCases('invalid')(cases),
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
"dependencies": {
"@babel/parser": "^7.3.2",
"@babel/traverse": "^7.2.3",
"common-prefix": "^1.1.0",
"pug-lexer": "^4.0.0",
"pug-lint": "^2.5.0",
"pug-uses-variables": "^3.0.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion tests/each-rule.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('each rule', () => {
describe('mentioned in README', () => {
const listOfRulesInReadme = fs
.readFileSync(path.resolve(__dirname, '..', 'README.md'), 'utf-8')
.match(/## List of supported rules\n+((\*.+\n+)+)/m)[1]
.match(/## List of supported rules\n+((\*.+\n+|^[^#]+)+)/m)[1]
.split('\n')
.filter(Boolean)

Expand Down
19 changes: 2 additions & 17 deletions tests/lib/rules/eslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

const eslint = require('eslint')
const buildError = require('../../../lib/util/testBuildError')
const buildCases = require('../../../lib/util/testBuildCases')

const { RuleTester } = eslint

Expand Down Expand Up @@ -216,20 +217,4 @@ const cases = [
},
]

const extractCase = type => item => ({
options: item.options || [],
...item[type],
})

const extractCases = type => (items) => {
if (items.some(item => item.only)) {
return items.filter(item => item.only).map(extractCase(type))
}

return items.map(extractCase(type))
}

ruleTester.run('rule "eslint"', rule, {
valid: extractCases('valid')(cases),
invalid: extractCases('invalid')(cases),
})
ruleTester.run('rule "eslint"', rule, buildCases(cases))
Loading