diff --git a/locales/en.json b/locales/en.json index 5baee0a7ec..0e6bbd37fc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -91,10 +91,11 @@ "property-missing-colon": "Put a colon (:) between the property and the value.\nTry: color: red", "selector-missing": "Start every block of CSS with a selector, such as an element name or class name.\nTry: p {\n color: red;\n}", "block-expected": "Start a block using { after your selector.\nTry: __error__ {", - "extra-tokens-after-value": "Looks like you're missing a semicolon on the line before this one.", + "extra-tokens-after-value": "The __token__ at the end of this line doesn’t belong there.", "illegal-token-after-combinator": "After a + or > in a selector, you need to specify the name of another element, class, or ID", "invalid-token": "This line doesn't look like valid CSS.", "invalid-value": "__error__ isn't a meaningful value for this property. Double-check what values you can use here.", + "missing-semicolon": "Looks like you’re missing a semicolon at the end of this line.", "require-value": "Put a value for __error__ after the colon.", "selector-expected": "Use a comma to separate multiple tag names, classes, or IDs.", "unknown-property": "__error__ isn't a property that CSS understands. Double-check the name of the property that you want to use." diff --git a/spec/assertions/validations.js b/spec/assertions/validations.js index 20f6ace296..6f18c49f89 100644 --- a/spec/assertions/validations.js +++ b/spec/assertions/validations.js @@ -1,7 +1,8 @@ import {assert} from 'chai'; import map from 'lodash/map'; +import trim from 'lodash/trim'; -function assertPassesValidation(validate, source) { +export function assertPassesValidation(validate, source) { return assert.eventually.deepEqual( validate(source), [], @@ -9,7 +10,7 @@ function assertPassesValidation(validate, source) { ); } -function assertFailsValidationWith(validate, source, ...reasons) { +export function assertFailsValidationWith(validate, source, ...reasons) { return assert.eventually.sameMembers( validate(source).then((errors) => map(errors, 'reason')), reasons, @@ -17,7 +18,10 @@ function assertFailsValidationWith(validate, source, ...reasons) { ); } -export { - assertPassesValidation, - assertFailsValidationWith, -}; +export function assertFailsValidationAtLine(validate, source, line) { + return assert.eventually.include( + validate(trim(source)).then((errors) => map(errors, 'row')), + line - 1, + `source fails validation at line: ${line}` + ); +} diff --git a/spec/examples/validations/css.spec.js b/spec/examples/validations/css.spec.js index 09286c23d5..aab3275184 100644 --- a/spec/examples/validations/css.spec.js +++ b/spec/examples/validations/css.spec.js @@ -4,21 +4,14 @@ import '../../helper'; import { assertPassesValidation, assertFailsValidationWith, + assertFailsValidationAtLine, } from '../../assertions/validations'; import css from '../../../src/validations/css'; -function assertPassesCssValidation(source) { - return assertPassesValidation(css, source); -} - -function assertFailsCssValidationWith(source, ...errors) { - return assertFailsValidationWith(css, source, ...errors); -} - describe('css', () => { it('allows valid flexbox', () => - assertPassesCssValidation(` + assertPassesValidation(css, ` .flex-container { display: flex; flex-flow: nowrap column; @@ -35,10 +28,44 @@ describe('css', () => { ); it('fails with bogus flex value', () => - assertFailsCssValidationWith( + assertFailsValidationWith( + css, '.flex-item { flex: bogus; }', 'invalid-value' ) ); + + context('missing semicolon', () => { + const stylesheet = ` + p { + margin: 10px + padding: 5px; + } + `; + + it('gives missing semicolon error', () => + assertFailsValidationWith(css, stylesheet, 'missing-semicolon') + ); + + it('fails at the line missing the semicolon', () => { + assertFailsValidationAtLine(css, stylesheet, 1); + }); + }); + + context('extra tokens after value', () => { + const stylesheet = ` + p { + padding: 5px 5px 5px 5px 5px; + } + `; + + it('gives extra tokens error', () => + assertFailsValidationWith(css, stylesheet, 'extra-tokens-after-value') + ); + + it('fails at the line missing the semicolon', () => + assertFailsValidationAtLine(css, stylesheet, 2) + ); + }); }); diff --git a/src/validations/Validator.js b/src/validations/Validator.js index 113ece56f1..d29bfea4f0 100644 --- a/src/validations/Validator.js +++ b/src/validations/Validator.js @@ -23,7 +23,7 @@ class Validator { _mapError(rawError) { const key = this._keyForError(rawError); if (this._errorMap.hasOwnProperty(key)) { - return this._errorMap[key](rawError); + return this._errorMap[key](rawError, this._source); } return null; } @@ -46,7 +46,7 @@ class Validator { const location = this._locationForError(rawError); - return assign(error, location, { + return assign({}, location, error, { text: message, raw: message, type: 'error', diff --git a/src/validations/css/prettycss.js b/src/validations/css/prettycss.js index e6dd2025a6..eb44ab1a27 100644 --- a/src/validations/css/prettycss.js +++ b/src/validations/css/prettycss.js @@ -1,5 +1,8 @@ import prettyCSS from 'PrettyCSS'; import Validator from '../Validator'; +import trim from 'lodash/trim'; +import startsWith from 'lodash/startsWith'; +import endsWith from 'lodash/endsWith'; const RADIAL_GRADIENT_EXPR = /^(?:(?:-(?:ms|moz|o|webkit)-)?radial-gradient|-webkit-gradient)/; @@ -13,9 +16,28 @@ const errorMap = { payload: {error: error.token.content}, }), - 'extra-tokens-after-value': () => ({ - reason: 'extra-tokens-after-value', - }), + 'extra-tokens-after-value': (error, source) => { + const lineNumber = error.token.line; + const lines = source.split('\n'); + const previousLine = lines[lineNumber - 2]; + const thisLine = lines[lineNumber - 1]; + + if ( + startsWith(trim(thisLine), error.token.content) && + !endsWith(trim(previousLine), ';') + ) { + return { + reason: 'missing-semicolon', + row: lineNumber - 2, + column: previousLine.length - 1, + }; + } + + return ({ + reason: 'extra-tokens-after-value', + payload: {token: error.token.content}, + }); + }, 'illegal-token-after-combinator': () => ({ reason: 'illegal-token-after-combinator',