Skip to content

Commit

Permalink
Lint JavaScript in Pug by providing eslint interface (#62)
Browse files Browse the repository at this point in the history
* Allow running mocha easier

* Add new rule: eslint

* Import eslint rule to the plugin definition

* Make sure we set correct line to reports

* Check out the code inside interpolation

* Always send reports with loc range

* Override breaking rules

* Remove unsupported rules

* Set correct location for object reports
  • Loading branch information
ezhlobo authored May 12, 2019
1 parent b1b4fec commit 47c7a21
Show file tree
Hide file tree
Showing 7 changed files with 437 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Then configure the rules you want to use under the rules section.
## List of supported rules

* [`react-pug/empty-lines`](./docs/rules/empty-lines.md): Manage empty lines in Pug
* [`react-pug/eslint`](./docs/rules/eslint.md): Lint JavaScript code inside Pug
* [`react-pug/indent`](./docs/rules/indent.md): Enforce consistent indentation
* [`react-pug/no-broken-template`](./docs/rules/no-broken-template.md): Disallow broken template
* [`react-pug/no-interpolation`](./docs/rules/no-interpolation.md): Disallow JavaScript interpolation
Expand Down
33 changes: 33 additions & 0 deletions docs/rules/eslint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Lint JavaScript code inside Pug (react-pug/eslint)

This rule applies eslint to JavaScript code used in Pug.

## Rule Details

The following patterns are considered warnings:

```jsx
/*eslint react-pug/eslint: ["error", { "comma-spacing": "error" }]*/
pug`div(data-value=getValueFor(one,two,three))`
```

```jsx
/*eslint react-pug/eslint: ["error", { "no-multi-spaces": "error" }]*/
pug`div(data-value=getValueFor(one,two,three, four, five))`
```

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

```jsx
/*eslint react-pug/eslint: ["error", { "comma-spacing": "error" }]*/
pug`div(data-value=getValueFor(one, two, three))`
```

```jsx
/*eslint react-pug/eslint: ["error", { "no-multi-spaces": "error" }]*/
pug`div(data-value=getValueFor(one,two,three, four, five))`
```

## When Not To Use It

If you don't won't to apply eslint rules to the javascript inside Pug.
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/* eslint-disable global-require */
const allRules = {
'empty-lines': require('./lib/rules/empty-lines'),
eslint: require('./lib/rules/eslint'),
indent: require('./lib/rules/indent'),
'no-broken-template': require('./lib/rules/no-broken-template'),
'no-interpolation': require('./lib/rules/no-interpolation'),
Expand All @@ -24,6 +25,7 @@ module.exports = {
},
rules: {
'react-pug/empty-lines': 2,
'react-pug/eslint': 2,
'react-pug/indent': 2,
'react-pug/no-broken-template': 2,
'react-pug/no-undef': 2,
Expand Down
133 changes: 133 additions & 0 deletions lib/rules/eslint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* @fileoverview Lint JavaScript code inside Pug
* @author Eugene Zhlobo
*/

// eslint-disable-next-line import/no-extraneous-dependencies
const { Linter } = require('eslint')

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

const linter = new Linter()

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

module.exports = {
meta: {
docs: {
description: 'Lint JavaScript code inside Pug',
category: 'Stylistic Issues',
recommended: false,
url: docsUrl('eslint'),
},
schema: [
{
type: 'object',
},
],
},

create: function (context) {
const lint = source => linter.verify(source, {
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
generators: false,
objectLiteralDuplicateProperties: false,
},
},
rules: {
...context.options[0],

// Controlled via other rules:
quotes: 'off',
'no-undef': 'off',

// To bypass literals and one-line source
'eol-last': 'off',
'no-unused-expressions': 'off',

// Unsupported features by pug
'prefer-template': 'off',
'prefer-destructuring': 'off',
},
})

const getStringToLint = (token) => {
switch (token.type) {
case 'each':
return token.code

case 'attribute':
case 'if':
case 'else-if':
case 'code':
case 'interpolated-code':
return token.val

default:
return null
}
}

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

tokens.forEach((token) => {
const rawSource = getStringToLint(token)

if (rawSource) {
const isMultiline = token.loc.start.line !== token.loc.end.line
const extraIndent = isMultiline
? token.loc.start.column - 1
: 0

const alignedSource = babelHelpers.align(rawSource, extraIndent)
const parsableSource = babelHelpers.normalize(alignedSource)

const reports = lint(parsableSource)

if (reports.length) {
reports.forEach((report) => {
const reportLine = token.loc.start.line - 1 + report.line - 1
const reportSource = alignedSource.split('\n')[report.line - 1]
const sourceStartColumn = lines[reportLine].indexOf(reportSource) - 1
const sourceStartLine = node.loc.start.line + token.loc.start.line - 2

const startLine = sourceStartLine + report.line
const endLine = sourceStartLine + (report.endLine || report.line)

const loc = report.endColumn
? buildLocation(
[startLine, sourceStartColumn + report.column],
[endLine, sourceStartColumn + report.endColumn],
)
: buildLocation(
[startLine, sourceStartColumn + 1],
[endLine, sourceStartColumn + reportSource.length + 1],
)

context.report({
node,
loc,
message: report.message,
})
})
}
}
})
}
},
}
},
}
31 changes: 31 additions & 0 deletions lib/util/babel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
function doesStringLookLikeObject(string) {
return /^\s*{/.test(string)
}

function normalize(value) {
// Just an object (like `{ first: 'one' }`) is not valid string to
// be parsed by babel, so we need to wrap it by braces
if (typeof value === 'string' && doesStringLookLikeObject(value)) {
return `(${value})`
}

// Pug can return a boolean variable for attribute value, but babel
// can parses only strings
return String(value)
}

function align(value, indent = 0) {
if (!indent) {
return value
}

return value
.split('\n')
.map(line => line.replace(new RegExp(`^\\s{${indent}}`), ''))
.join('\n')
}

module.exports = {
normalize,
align,
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
],
"scripts": {
"lint": "eslint ./",
"test": "mocha 'tests/**/*.js'"
"test": "mocha 'tests/**/*.js'",
"mocha": "mocha"
},
"dependencies": {
"@babel/parser": "^7.3.2",
Expand Down
Loading

0 comments on commit 47c7a21

Please sign in to comment.