Merge pull request #1340 from sveltejs/gh-474

add codes to warnings
pull/1345/head
Rich Harris 7 years ago committed by GitHub
commit 7fe139feaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -102,7 +102,10 @@ export default class Selector {
while (i-- > 1) { while (i-- > 1) {
const selector = block.selectors[i]; const selector = block.selectors[i];
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
validator.error(`:global(...) must be the first element in a compound selector`, selector); validator.error(selector, {
code: `css-invalid-global`,
message: `:global(...) must be the first element in a compound selector`
});
} }
} }
}); });
@ -120,7 +123,10 @@ export default class Selector {
for (let i = start; i < end; i += 1) { for (let i = start; i < end; i += 1) {
if (this.blocks[i].global) { if (this.blocks[i].global) {
validator.error(`:global(...) can be at the start or end of a selector sequence, but not in the middle`, this.blocks[i].selectors[0]); validator.error(this.blocks[i].selectors[0], {
code: `css-invalid-global`,
message: `:global(...) can be at the start or end of a selector sequence, but not in the middle`
});
} }
} }
} }

@ -435,6 +435,7 @@ export default class Stylesheet {
const message = `Unused CSS selector`; const message = `Unused CSS selector`;
onwarn({ onwarn({
code: `css-unused-selector`,
message, message,
frame, frame,
loc: { line: line + 1, column }, loc: { line: line + 1, column },

@ -276,6 +276,7 @@ function getGlobals(dependencies: Dependency[], options: CompileOptions) {
onerror(error); onerror(error);
} else { } else {
const warning = { const warning = {
code: `options-missing-globals`,
message: `No name was supplied for imported module '${d.source}'. Guessing '${d.name}', but you should use options.globals`, message: `No name was supplied for imported module '${d.source}'. Guessing '${d.name}', but you should use options.globals`,
}; };

@ -31,6 +31,7 @@ export interface Warning {
loc?: { line: number; column: number; pos?: number }; loc?: { line: number; column: number; pos?: number };
end?: { line: number; column: number; }; end?: { line: number; column: number; };
pos?: number; pos?: number;
code: string;
message: string; message: string;
filename?: string; filename?: string;
frame?: string; frame?: string;

@ -71,11 +71,19 @@ export class Parser {
const current = this.current(); const current = this.current();
const type = current.type === 'Element' ? `<${current.name}>` : 'Block'; const type = current.type === 'Element' ? `<${current.name}>` : 'Block';
this.error(`${type} was left open`, current.start); const slug = current.type === 'Element' ? 'element' : 'block';
this.error({
code: `unclosed-${slug}`,
message: `${type} was left open`
}, current.start);
} }
if (state !== fragment) { if (state !== fragment) {
this.error('Unexpected end of input'); this.error({
code: `unexpected-eof`,
message: 'Unexpected end of input'
});
} }
if (this.html.children.length) { if (this.html.children.length) {
@ -97,12 +105,16 @@ export class Parser {
} }
acornError(err: any) { acornError(err: any) {
this.error(err.message.replace(/ \(\d+:\d+\)$/, ''), err.pos); this.error({
code: `parse-error`,
message: err.message.replace(/ \(\d+:\d+\)$/, '')
}, err.pos);
} }
error(message: string, index = this.index) { error({ code, message }: { code: string, message: string }, index = this.index) {
error(message, { error(message, {
name: 'ParseError', name: 'ParseError',
code,
source: this.template, source: this.template,
start: index, start: index,
filename: this.filename filename: this.filename
@ -116,7 +128,10 @@ export class Parser {
} }
if (required) { if (required) {
this.error(message || `Expected ${str}`); this.error({
code: `unexpected-${this.index === this.template.length ? 'eof' : 'token'}`,
message: message || `Expected ${str}`
});
} }
return false; return false;
@ -164,7 +179,10 @@ export class Parser {
const identifier = this.template.slice(this.index, this.index = i); const identifier = this.template.slice(this.index, this.index = i);
if (reservedNames.has(identifier)) { if (reservedNames.has(identifier)) {
this.error(`'${identifier}' is a reserved word in JavaScript and cannot be used here`, start); this.error({
code: `unexpected-reserved-word`,
message: `'${identifier}' is a reserved word in JavaScript and cannot be used here`
}, start);
} }
return identifier; return identifier;
@ -172,7 +190,10 @@ export class Parser {
readUntil(pattern: RegExp) { readUntil(pattern: RegExp) {
if (this.index >= this.template.length) if (this.index >= this.template.length)
this.error('Unexpected end of input'); this.error({
code: `unexpected-eof`,
message: 'Unexpected end of input'
});
const start = this.index; const start = this.index;
const match = pattern.exec(this.template.slice(start)); const match = pattern.exec(this.template.slice(start));
@ -192,7 +213,10 @@ export class Parser {
requireWhitespace() { requireWhitespace() {
if (!whitespace.test(this.template[this.index])) { if (!whitespace.test(this.template[this.index])) {
this.error(`Expected whitespace`); this.error({
code: `missing-whitespace`,
message: `Expected whitespace`
});
} }
this.allowWhitespace(); this.allowWhitespace();

@ -2,7 +2,19 @@ import { parseExpressionAt } from 'acorn';
import repeat from '../../utils/repeat'; import repeat from '../../utils/repeat';
import { Parser } from '../index'; import { Parser } from '../index';
const DIRECTIVES = { const DIRECTIVES: Record<string, {
names: string[];
attribute: (
start: number,
end: number,
type: string,
name: string,
expression?: any,
directiveName?: string
) => { start: number, end: number, type: string, name: string, value?: any, expression?: any };
allowedExpressionTypes: string[];
error: string;
}> = {
Ref: { Ref: {
names: ['ref'], names: ['ref'],
attribute(start, end, type, name) { attribute(start, end, type, name) {
@ -143,7 +155,10 @@ export function readDirective(
try { try {
expression = readExpression(parser, expressionStart, quoteMark); expression = readExpression(parser, expressionStart, quoteMark);
if (directive.allowedExpressionTypes.indexOf(expression.type) === -1) { if (directive.allowedExpressionTypes.indexOf(expression.type) === -1) {
parser.error(directive.error, expressionStart); parser.error({
code: `invalid-directive-value`,
message: directive.error
}, expressionStart);
} }
} catch (err) { } catch (err) {
if (parser.template[expressionStart] === '{') { if (parser.template[expressionStart] === '{') {
@ -155,7 +170,10 @@ export function readDirective(
const value = parser.template.slice(expressionStart + (parser.v2 ? 1 : 2), expressionEnd); const value = parser.template.slice(expressionStart + (parser.v2 ? 1 : 2), expressionEnd);
message += ` — use '${value}', not '${parser.v2 ? `{${value}}` : `{{${value}}}`}'`; message += ` — use '${value}', not '${parser.v2 ? `{${value}}` : `{{${value}}}`}'`;
} }
parser.error(message, expressionStart); parser.error({
code: `invalid-directive-value`,
message
}, expressionStart);
} }
throw err; throw err;

@ -12,7 +12,10 @@ export default function readScript(parser: Parser, start: number, attributes: No
const scriptStart = parser.index; const scriptStart = parser.index;
const scriptEnd = parser.template.indexOf(scriptClosingTag, scriptStart); const scriptEnd = parser.template.indexOf(scriptClosingTag, scriptStart);
if (scriptEnd === -1) parser.error(`<script> must have a closing tag`); if (scriptEnd === -1) parser.error({
code: `unclosed-script`,
message: `<script> must have a closing tag`
});
const source = const source =
repeat(' ', scriptStart) + parser.template.slice(scriptStart, scriptEnd); repeat(' ', scriptStart) + parser.template.slice(scriptStart, scriptEnd);

@ -17,7 +17,10 @@ export default function readStyle(parser: Parser, start: number, attributes: Nod
}); });
} catch (err) { } catch (err) {
if (err.name === 'CssSyntaxError') { if (err.name === 'CssSyntaxError') {
parser.error(err.message, err.offset); parser.error({
code: `css-syntax-error`,
message: err.message
}, err.offset);
} else { } else {
throw err; throw err;
} }

@ -56,7 +56,10 @@ export default function mustache(parser: Parser) {
} else if (block.type === 'AwaitBlock') { } else if (block.type === 'AwaitBlock') {
expected = 'await'; expected = 'await';
} else { } else {
parser.error(`Unexpected block closing tag`); parser.error({
code: `unexpected-block-close`,
message: `Unexpected block closing tag`
});
} }
parser.eat(expected, true); parser.eat(expected, true);
@ -86,9 +89,10 @@ export default function mustache(parser: Parser) {
} else if (parser.eat(parser.v2 ? ':elseif' : 'elseif')) { } else if (parser.eat(parser.v2 ? ':elseif' : 'elseif')) {
const block = parser.current(); const block = parser.current();
if (block.type !== 'IfBlock') if (block.type !== 'IfBlock')
parser.error( parser.error({
'Cannot have an {{elseif ...}} block outside an {{#if ...}} block' code: `invalid-elseif-placement`,
); message: 'Cannot have an {{elseif ...}} block outside an {{#if ...}} block'
});
parser.requireWhitespace(); parser.requireWhitespace();
@ -117,9 +121,10 @@ export default function mustache(parser: Parser) {
} else if (parser.eat(parser.v2 ? ':else' : 'else')) { } else if (parser.eat(parser.v2 ? ':else' : 'else')) {
const block = parser.current(); const block = parser.current();
if (block.type !== 'IfBlock' && block.type !== 'EachBlock') { if (block.type !== 'IfBlock' && block.type !== 'EachBlock') {
parser.error( parser.error({
'Cannot have an {{else}} block outside an {{#if ...}} or {{#each ...}} block' code: `invalid-else-placement`,
); message: 'Cannot have an {{else}} block outside an {{#if ...}} or {{#each ...}} block'
});
} }
parser.allowWhitespace(); parser.allowWhitespace();
@ -191,7 +196,10 @@ export default function mustache(parser: Parser) {
} else if (parser.eat('await')) { } else if (parser.eat('await')) {
type = 'AwaitBlock'; type = 'AwaitBlock';
} else { } else {
parser.error(`Expected if, each or await`); parser.error({
code: `expected-block-type`,
message: `Expected if, each or await`
});
} }
parser.requireWhitespace(); parser.requireWhitespace();
@ -249,20 +257,30 @@ export default function mustache(parser: Parser) {
parser.allowWhitespace(); parser.allowWhitespace();
const destructuredContext = parser.readIdentifier(); const destructuredContext = parser.readIdentifier();
if (!destructuredContext) parser.error(`Expected name`); if (!destructuredContext) parser.error({
code: `expected-name`,
message: `Expected name`
});
block.destructuredContexts.push(destructuredContext); block.destructuredContexts.push(destructuredContext);
parser.allowWhitespace(); parser.allowWhitespace();
} while (parser.eat(',')); } while (parser.eat(','));
if (!block.destructuredContexts.length) parser.error(`Expected name`); if (!block.destructuredContexts.length) parser.error({
code: `expected-name`,
message: `Expected name`
});
block.context = block.destructuredContexts.join('_'); block.context = block.destructuredContexts.join('_');
parser.allowWhitespace(); parser.allowWhitespace();
parser.eat(']', true); parser.eat(']', true);
} else { } else {
block.context = parser.readIdentifier(); block.context = parser.readIdentifier();
if (!block.context) parser.error(`Expected name`); if (!block.context) parser.error({
code: `expected-name`,
message: `Expected name`
});
} }
parser.allowWhitespace(); parser.allowWhitespace();
@ -270,7 +288,11 @@ export default function mustache(parser: Parser) {
if (parser.eat(',')) { if (parser.eat(',')) {
parser.allowWhitespace(); parser.allowWhitespace();
block.index = parser.readIdentifier(); block.index = parser.readIdentifier();
if (!block.index) parser.error(`Expected name`); if (!block.index) parser.error({
code: `expected-name`,
message: `Expected name`
});
parser.allowWhitespace(); parser.allowWhitespace();
} }
@ -287,7 +309,10 @@ export default function mustache(parser: Parser) {
expression.property.computed || expression.property.computed ||
expression.property.type !== 'Identifier' expression.property.type !== 'Identifier'
) { ) {
parser.error('invalid key', expression.start); parser.error({
code: `invalid-key`,
message: 'invalid key'
}, expression.start);
} }
block.key = expression.property.name; block.key = expression.property.name;
@ -296,7 +321,10 @@ export default function mustache(parser: Parser) {
parser.allowWhitespace(); parser.allowWhitespace();
} else if (parser.eat('@')) { } else if (parser.eat('@')) {
block.key = parser.readIdentifier(); block.key = parser.readIdentifier();
if (!block.key) parser.error(`Expected name`); if (!block.key) parser.error({
code: `expected-name`,
message: `Expected name`
});
parser.allowWhitespace(); parser.allowWhitespace();
} }
} }

@ -83,21 +83,27 @@ export default function tag(parser: Parser) {
const name = readTagName(parser); const name = readTagName(parser);
if (metaTags.has(name)) { if (metaTags.has(name)) {
const slug = metaTags.get(name).toLowerCase();
if (isClosingTag) { if (isClosingTag) {
if ((name === ':Window' || name === 'svelte:window') && parser.current().children.length) { if ((name === ':Window' || name === 'svelte:window') && parser.current().children.length) {
parser.error( parser.error({
`<${name}> cannot have children`, code: `invalid-window-content`,
parser.current().children[0].start message: `<${name}> cannot have children`
); }, parser.current().children[0].start);
} }
} else { } else {
if (name in parser.metaTags) { if (name in parser.metaTags) {
parser.error(`A component can only have one <${name}> tag`, start); parser.error({
code: `duplicate-${slug}`,
message: `A component can only have one <${name}> tag`
}, start);
} }
if (parser.stack.length > 1) { if (parser.stack.length > 1) {
console.log(parser.stack); parser.error({
parser.error(`<${name}> tags cannot be inside elements or blocks`, start); code: `invalid-${slug}-placement`,
message: `<${name}> tags cannot be inside elements or blocks`
}, start);
} }
parser.metaTags[name] = true; parser.metaTags[name] = true;
@ -121,21 +127,21 @@ export default function tag(parser: Parser) {
if (isClosingTag) { if (isClosingTag) {
if (isVoidElementName(name)) { if (isVoidElementName(name)) {
parser.error( parser.error({
`<${name}> is a void element and cannot have children, or a closing tag`, code: `invalid-void-content`,
start message: `<${name}> is a void element and cannot have children, or a closing tag`
); }, start);
} }
if (!parser.eat('>')) parser.error(`Expected '>'`); parser.eat('>', true);
// close any elements that don't have their own closing tags, e.g. <div><p></div> // close any elements that don't have their own closing tags, e.g. <div><p></div>
while (parent.name !== name) { while (parent.name !== name) {
if (parent.type !== 'Element') if (parent.type !== 'Element')
parser.error( parser.error({
`</${name}> attempted to close an element that was not open`, code: `invalid-closing-tag`,
start message: `</${name}> attempted to close an element that was not open`
); }, start);
parent.end = start; parent.end = start;
parser.stack.pop(); parser.stack.pop();
@ -161,10 +167,10 @@ export default function tag(parser: Parser) {
while (i--) { while (i--) {
const item = parser.stack[i]; const item = parser.stack[i];
if (item.type === 'EachBlock') { if (item.type === 'EachBlock') {
parser.error( parser.error({
`<slot> cannot be a child of an each-block`, code: `invalid-slot-placement`,
start message: `<slot> cannot be a child of an each-block`
); }, start);
} }
} }
} }
@ -182,7 +188,10 @@ export default function tag(parser: Parser) {
let attribute; let attribute;
while ((attribute = readAttribute(parser, uniqueNames))) { while ((attribute = readAttribute(parser, uniqueNames))) {
if (attribute.type === 'Binding' && !parser.allowBindings) { if (attribute.type === 'Binding' && !parser.allowBindings) {
parser.error(`Two-way binding is disabled`, attribute.start); parser.error({
code: `binding-disabled`,
message: `Two-way binding is disabled`
}, attribute.start);
} }
element.attributes.push(attribute); element.attributes.push(attribute);
@ -193,12 +202,18 @@ export default function tag(parser: Parser) {
// TODO post v2, treat this just as any other attribute // TODO post v2, treat this just as any other attribute
const index = element.attributes.findIndex(attr => attr.name === 'this'); const index = element.attributes.findIndex(attr => attr.name === 'this');
if (!~index) { if (!~index) {
parser.error(`<svelte:component> must have a 'this' attribute`, start); parser.error({
code: `missing-component-definition`,
message: `<svelte:component> must have a 'this' attribute`
}, start);
} }
const definition = element.attributes.splice(index, 1)[0]; const definition = element.attributes.splice(index, 1)[0];
if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') { if (definition.value === true || definition.value.length !== 1 || definition.value[0].type === 'Text') {
parser.error(`invalid component definition`, definition.start); parser.error({
code: `invalid-component-definition`,
message: `invalid component definition`
}, definition.start);
} }
element.expression = definition.value[0].expression; element.expression = definition.value[0].expression;
@ -210,9 +225,10 @@ export default function tag(parser: Parser) {
if (parser[special.property]) { if (parser[special.property]) {
parser.index = start; parser.index = start;
parser.error( parser.error({
`You can only have one top-level <${name}> tag per component` code: `duplicate-${name}`,
); message: `You can only have one top-level <${name}> tag per component`
});
} }
parser.eat('>', true); parser.eat('>', true);
@ -290,10 +306,10 @@ function readTagName(parser: Parser) {
} }
if (!legal) { if (!legal) {
parser.error( parser.error({
`<${SELF}> components can only exist inside if-blocks or each-blocks`, code: `invalid-self-placement`,
start message: `<${SELF}> components can only exist inside if-blocks or each-blocks`
); }, start);
} }
return SELF; return SELF;
@ -306,7 +322,10 @@ function readTagName(parser: Parser) {
if (metaTags.has(name)) return name; if (metaTags.has(name)) return name;
if (!validTagName.test(name)) { if (!validTagName.test(name)) {
parser.error(`Expected valid tag name`, start); parser.error({
code: `invalid-tag-name`,
message: `Expected valid tag name`
}, start);
} }
return name; return name;
@ -332,7 +351,10 @@ function readAttribute(parser: Parser, uniqueNames: Set<string>) {
}; };
} else { } else {
if (!parser.v2) { if (!parser.v2) {
parser.error('Expected spread operator (...)'); parser.error({
code: `expected-spread`,
message: 'Expected spread operator (...)'
});
} }
const valueStart = parser.index; const valueStart = parser.index;
@ -364,7 +386,10 @@ function readAttribute(parser: Parser, uniqueNames: Set<string>) {
let name = parser.readUntil(/(\s|=|\/|>)/); let name = parser.readUntil(/(\s|=|\/|>)/);
if (!name) return null; if (!name) return null;
if (uniqueNames.has(name)) { if (uniqueNames.has(name)) {
parser.error('Attributes need to be unique', start); parser.error({
code: `duplicate-attribute`,
message: 'Attributes need to be unique'
}, start);
} }
uniqueNames.add(name); uniqueNames.add(name);
@ -452,5 +477,8 @@ function readSequence(parser: Parser, done: () => boolean) {
} }
} }
parser.error(`Unexpected end of input`); parser.error({
code: `unexpected-eof`,
message: `Unexpected end of input`
});
} }

@ -2,6 +2,7 @@ import { locate } from 'locate-character';
import getCodeFrame from '../utils/getCodeFrame'; import getCodeFrame from '../utils/getCodeFrame';
class CompileError extends Error { class CompileError extends Error {
code: string;
loc: { line: number, column: number }; loc: { line: number, column: number };
end: { line: number, column: number }; end: { line: number, column: number };
pos: number; pos: number;
@ -15,6 +16,7 @@ class CompileError extends Error {
export default function error(message: string, props: { export default function error(message: string, props: {
name: string, name: string,
code: string,
source: string, source: string,
filename: string, filename: string,
start: number, start: number,
@ -26,6 +28,7 @@ export default function error(message: string, props: {
const start = locate(props.source, props.start); const start = locate(props.source, props.start);
const end = locate(props.source, props.end || props.start); const end = locate(props.source, props.end || props.start);
error.code = props.code;
error.loc = { line: start.line + 1, column: start.column }; error.loc = { line: start.line + 1, column: start.column };
error.end = { line: end.line + 1, column: end.column }; error.end = { line: end.line + 1, column: end.column };
error.pos = props.start; error.pos = props.start;

@ -35,7 +35,10 @@ export default function a11y(
if (name.startsWith('aria-')) { if (name.startsWith('aria-')) {
if (invisibleElements.has(node.name)) { if (invisibleElements.has(node.name)) {
// aria-unsupported-elements // aria-unsupported-elements
validator.warn(`A11y: <${node.name}> should not have aria-* attributes`, attribute); validator.warn(attribute, {
code: `a11y-aria-attributes`,
message: `A11y: <${node.name}> should not have aria-* attributes`
});
} }
const type = name.slice(5); const type = name.slice(5);
@ -44,7 +47,10 @@ export default function a11y(
let message = `A11y: Unknown aria attribute 'aria-${type}'`; let message = `A11y: Unknown aria attribute 'aria-${type}'`;
if (match) message += ` (did you mean '${match}'?)`; if (match) message += ` (did you mean '${match}'?)`;
validator.warn(message, attribute); validator.warn(attribute, {
code: `a11y-unknown-aria-attribute`,
message
});
} }
} }
@ -52,7 +58,10 @@ export default function a11y(
if (name === 'role') { if (name === 'role') {
if (invisibleElements.has(node.name)) { if (invisibleElements.has(node.name)) {
// aria-unsupported-elements // aria-unsupported-elements
validator.warn(`A11y: <${node.name}> should not have role attribute`, attribute); validator.warn(attribute, {
code: `a11y-misplaced-role`,
message: `A11y: <${node.name}> should not have role attribute`
});
} }
const value = getStaticAttributeValue(node, 'role'); const value = getStaticAttributeValue(node, 'role');
@ -61,30 +70,45 @@ export default function a11y(
let message = `A11y: Unknown role '${value}'`; let message = `A11y: Unknown role '${value}'`;
if (match) message += ` (did you mean '${match}'?)`; if (match) message += ` (did you mean '${match}'?)`;
validator.warn(message, attribute); validator.warn(attribute, {
code: `a11y-unknown-role`,
message
});
} }
} }
// no-access-key // no-access-key
if (name === 'accesskey') { if (name === 'accesskey') {
validator.warn(`A11y: Avoid using accesskey`, attribute); validator.warn(attribute, {
code: `a11y-accesskey`,
message: `A11y: Avoid using accesskey`
});
} }
// no-autofocus // no-autofocus
if (name === 'autofocus') { if (name === 'autofocus') {
validator.warn(`A11y: Avoid using autofocus`, attribute); validator.warn(attribute, {
code: `a11y-autofocus`,
message: `A11y: Avoid using autofocus`
});
} }
// scope // scope
if (name === 'scope' && node.name !== 'th') { if (name === 'scope' && node.name !== 'th') {
validator.warn(`A11y: The scope attribute should only be used with <th> elements`, attribute); validator.warn(attribute, {
code: `a11y-misplaced-scope`,
message: `A11y: The scope attribute should only be used with <th> elements`
});
} }
// tabindex-no-positive // tabindex-no-positive
if (name === 'tabindex') { if (name === 'tabindex') {
const value = getStaticAttributeValue(node, 'tabindex'); const value = getStaticAttributeValue(node, 'tabindex');
if (!isNaN(value) && +value > 0) { if (!isNaN(value) && +value > 0) {
validator.warn(`A11y: avoid tabindex values above zero`, attribute); validator.warn(attribute, {
code: `a11y-positive-tabindex`,
message: `A11y: avoid tabindex values above zero`
});
} }
} }
@ -98,13 +122,19 @@ export default function a11y(
attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` : attributes.slice(0, -1).join(', ') + ` or ${attributes[attributes.length - 1]}` :
attributes[0]; attributes[0];
validator.warn(`A11y: <${name}> element should have ${article} ${sequence} attribute`, node); validator.warn(node, {
code: `a11y-missing-attribute`,
message: `A11y: <${name}> element should have ${article} ${sequence} attribute`
});
} }
} }
function shouldHaveContent() { function shouldHaveContent() {
if (node.children.length === 0) { if (node.children.length === 0) {
validator.warn(`A11y: <${node.name}> element should have child content`, node); validator.warn(node, {
code: `a11y-missing-content`,
message: `A11y: <${node.name}> element should have child content`
});
} }
} }
@ -112,7 +142,10 @@ export default function a11y(
const href = attributeMap.get(attribute); const href = attributeMap.get(attribute);
const value = getStaticAttributeValue(node, attribute); const value = getStaticAttributeValue(node, attribute);
if (value === '' || value === '#') { if (value === '' || value === '#') {
validator.warn(`A11y: '${value}' is not a valid ${attribute} attribute`, href); validator.warn(href, {
code: `a11y-invalid-attribute`,
message: `A11y: '${value}' is not a valid ${attribute} attribute`
});
} }
} }
@ -124,7 +157,10 @@ export default function a11y(
// anchor-in-svg-is-valid // anchor-in-svg-is-valid
shouldHaveValidHref('xlink:href') shouldHaveValidHref('xlink:href')
} else { } else {
validator.warn(`A11y: <a> element should have an href attribute`, node); validator.warn(node, {
code: `a11y-missing-attribute`,
message: `A11y: <a> element should have an href attribute`
});
} }
// anchor-has-content // anchor-has-content
@ -143,7 +179,10 @@ export default function a11y(
shouldHaveContent(); shouldHaveContent();
if (attributeMap.has('aria-hidden')) { if (attributeMap.has('aria-hidden')) {
validator.warn(`A11y: <${node.name}> element should not be hidden`, attributeMap.get('aria-hidden')); validator.warn(attributeMap.get('aria-hidden'), {
code: `a11y-hidden`,
message: `A11y: <${node.name}> element should not be hidden`
});
} }
} }
@ -159,14 +198,20 @@ export default function a11y(
// no-distracting-elements // no-distracting-elements
if (node.name === 'marquee' || node.name === 'blink') { if (node.name === 'marquee' || node.name === 'blink') {
validator.warn(`A11y: Avoid <${node.name}> elements`, node); validator.warn(node, {
code: `a11y-distracting-elements`,
message: `A11y: Avoid <${node.name}> elements`
});
} }
if (node.name === 'figcaption') { if (node.name === 'figcaption') {
const parent = elementStack[elementStack.length - 1]; const parent = elementStack[elementStack.length - 1];
if (parent) { if (parent) {
if (parent.name !== 'figure') { if (parent.name !== 'figure') {
validator.warn(`A11y: <figcaption> must be an immediate child of <figure>`, node); validator.warn(node, {
code: `a11y-structure`,
message: `A11y: <figcaption> must be an immediate child of <figure>`
});
} else { } else {
const children = parent.children.filter(node => { const children = parent.children.filter(node => {
if (node.type === 'Comment') return false; if (node.type === 'Comment') return false;
@ -177,7 +222,10 @@ export default function a11y(
const index = children.indexOf(node); const index = children.indexOf(node);
if (index !== 0 && index !== children.length - 1) { if (index !== 0 && index !== children.length - 1) {
validator.warn(`A11y: <figcaption> must be first or last child of <figure>`, node); validator.warn(node, {
code: `a11y-structure`,
message: `A11y: <figcaption> must be first or last child of <figure>`
});
} }
} }
} }

@ -61,15 +61,18 @@ export default function validateHtml(validator: Validator, html: Node) {
c += 2; c += 2;
while (/\s/.test(validator.source[c])) c += 1; while (/\s/.test(validator.source[c])) c += 1;
validator.warn( validator.warn({ start: c, end: c + node.context.length }, {
`Context clashes with a helper. Rename one or the other to eliminate any ambiguity`, code: `each-context-clash`,
{ start: c, end: c + node.context.length } message: `Context clashes with a helper. Rename one or the other to eliminate any ambiguity`
); });
} }
} }
if (validator.options.dev && isEmptyBlock(node)) { if (validator.options.dev && isEmptyBlock(node)) {
validator.warn('Empty block', node); validator.warn(node, {
code: `empty-block`,
message: 'Empty block'
});
} }
if (node.children) { if (node.children) {
@ -105,7 +108,10 @@ export default function validateHtml(validator: Validator, html: Node) {
let message = `'refs.${ref}' does not exist`; let message = `'refs.${ref}' does not exist`;
if (match) message += ` (did you mean 'refs.${match}'?)`; if (match) message += ` (did you mean 'refs.${match}'?)`;
validator.error(message, callee); validator.error(callee, {
code: `missing-ref`,
message
});
} }
}); });
} }

@ -20,26 +20,35 @@ export default function validateElement(
if (!isComponent && /^[A-Z]/.test(node.name[0])) { if (!isComponent && /^[A-Z]/.test(node.name[0])) {
// TODO upgrade to validator.error in v2 // TODO upgrade to validator.error in v2
validator.warn(`${node.name} component is not defined`, node); validator.warn(node, {
code: `missing-component`,
message: `${node.name} component is not defined`
});
} }
if (elementStack.length === 0 && validator.namespace !== namespaces.svg && svg.test(node.name)) { if (elementStack.length === 0 && validator.namespace !== namespaces.svg && svg.test(node.name)) {
validator.warn( validator.warn(node, {
`<${node.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`, code: `missing-namespace`,
node message: `<${node.name}> is an SVG element did you forget to add { namespace: 'svg' } ?`
); });
} }
if (node.name === 'slot') { if (node.name === 'slot') {
const nameAttribute = node.attributes.find((attribute: Node) => attribute.name === 'name'); const nameAttribute = node.attributes.find((attribute: Node) => attribute.name === 'name');
if (nameAttribute) { if (nameAttribute) {
if (nameAttribute.value.length !== 1 || nameAttribute.value[0].type !== 'Text') { if (nameAttribute.value.length !== 1 || nameAttribute.value[0].type !== 'Text') {
validator.error(`<slot> name cannot be dynamic`, nameAttribute); validator.error(nameAttribute, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
});
} }
const slotName = nameAttribute.value[0].data; const slotName = nameAttribute.value[0].data;
if (slotName === 'default') { if (slotName === 'default') {
validator.error(`default is a reserved word — it cannot be used as a slot name`, nameAttribute); validator.error(nameAttribute, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
});
} }
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a // TODO should duplicate slots be disallowed? Feels like it's more likely to be a
@ -61,18 +70,18 @@ export default function validateElement(
if (node.name === 'title') { if (node.name === 'title') {
if (node.attributes.length > 0) { if (node.attributes.length > 0) {
validator.error( validator.error(node.attributes[0], {
`<title> cannot have attributes`, code: `illegal-attribute`,
node.attributes[0] message: `<title> cannot have attributes`
); });
} }
node.children.forEach(child => { node.children.forEach(child => {
if (child.type !== 'Text' && child.type !== 'MustacheTag') { if (child.type !== 'Text' && child.type !== 'MustacheTag') {
validator.error( validator.error(child, {
`<title> can only contain text and {{tags}}`, code: 'illegal-structure',
child message: `<title> can only contain text and {{tags}}`
); });
} }
}); });
} }
@ -96,10 +105,10 @@ export default function validateElement(
node.name !== 'textarea' && node.name !== 'textarea' &&
node.name !== 'select' node.name !== 'select'
) { ) {
validator.error( validator.error(attribute, {
`'value' is not a valid binding on <${node.name}> elements`, code: `invalid-binding`,
attribute message: `'value' is not a valid binding on <${node.name}> elements`
); });
} }
if (node.name === 'select') { if (node.name === 'select') {
@ -108,43 +117,43 @@ export default function validateElement(
); );
if (attribute && isDynamic(attribute)) { if (attribute && isDynamic(attribute)) {
validator.error( validator.error(attribute, {
`'multiple' attribute cannot be dynamic if select uses two-way binding`, code: `dynamic-multiple-attribute`,
attribute message: `'multiple' attribute cannot be dynamic if select uses two-way binding`
); });
} }
} else { } else {
checkTypeAttribute(validator, node); checkTypeAttribute(validator, node);
} }
} else if (name === 'checked' || name === 'indeterminate') { } else if (name === 'checked' || name === 'indeterminate') {
if (node.name !== 'input') { if (node.name !== 'input') {
validator.error( validator.error(attribute, {
`'${name}' is not a valid binding on <${node.name}> elements`, code: `invalid-binding`,
attribute message: `'${name}' is not a valid binding on <${node.name}> elements`
); });
} }
if (checkTypeAttribute(validator, node) !== 'checkbox') { if (checkTypeAttribute(validator, node) !== 'checkbox') {
validator.error( validator.error(attribute, {
`'${name}' binding can only be used with <input type="checkbox">`, code: `invalid-binding`,
attribute message: `'${name}' binding can only be used with <input type="checkbox">`
); });
} }
} else if (name === 'group') { } else if (name === 'group') {
if (node.name !== 'input') { if (node.name !== 'input') {
validator.error( validator.error(attribute, {
`'group' is not a valid binding on <${node.name}> elements`, code: `invalid-binding`,
attribute message: `'group' is not a valid binding on <${node.name}> elements`
); });
} }
const type = checkTypeAttribute(validator, node); const type = checkTypeAttribute(validator, node);
if (type !== 'checkbox' && type !== 'radio') { if (type !== 'checkbox' && type !== 'radio') {
validator.error( validator.error(attribute, {
`'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`, code: `invalid-binding`,
attribute message: `'checked' binding can only be used with <input type="checkbox"> or <input type="radio">`
); });
} }
} else if ( } else if (
name === 'currentTime' || name === 'currentTime' ||
@ -156,23 +165,26 @@ export default function validateElement(
name === 'volume' name === 'volume'
) { ) {
if (node.name !== 'audio' && node.name !== 'video') { if (node.name !== 'audio' && node.name !== 'video') {
validator.error( validator.error(attribute, {
`'${name}' binding can only be used with <audio> or <video>`, code: `invalid-binding`,
attribute message: `'${name}' binding can only be used with <audio> or <video>`
); });
} }
} else { } else {
validator.error( validator.error(attribute, {
`'${attribute.name}' is not a valid binding`, code: `invalid-binding`,
attribute message: `'${attribute.name}' is not a valid binding`
); });
} }
} else if (attribute.type === 'EventHandler') { } else if (attribute.type === 'EventHandler') {
validator.used.events.add(attribute.name); validator.used.events.add(attribute.name);
validateEventHandler(validator, attribute, refCallees); validateEventHandler(validator, attribute, refCallees);
} else if (attribute.type === 'Transition') { } else if (attribute.type === 'Transition') {
if (isComponent) { if (isComponent) {
validator.error(`Transitions can only be applied to DOM elements, not components`, attribute); validator.error(attribute, {
code: `invalid-transition`,
message: `Transitions can only be applied to DOM elements, not components`
});
} }
validator.used.transitions.add(attribute.name); validator.used.transitions.add(attribute.name);
@ -180,31 +192,31 @@ export default function validateElement(
const bidi = attribute.intro && attribute.outro; const bidi = attribute.intro && attribute.outro;
if (hasTransition) { if (hasTransition) {
if (bidi) if (bidi) {
validator.error( validator.error(attribute, {
`An element can only have one 'transition' directive`, code: `duplicate-transition`,
attribute message: `An element can only have one 'transition' directive`
); });
validator.error( }
`An element cannot have both a 'transition' directive and an '${attribute.intro
? 'in' validator.error(attribute, {
: 'out'}' directive`, code: `duplicate-transition`,
attribute message: `An element cannot have both a 'transition' directive and an '${attribute.intro ? 'in' : 'out'}' directive`
); });
} }
if ((hasIntro && attribute.intro) || (hasOutro && attribute.outro)) { if ((hasIntro && attribute.intro) || (hasOutro && attribute.outro)) {
if (bidi) if (bidi) {
validator.error( validator.error(attribute, {
`An element cannot have both an '${hasIntro code: `duplicate-transition`,
? 'in' message: `An element cannot have both an '${hasIntro ? 'in' : 'out'}' directive and a 'transition' directive`
: 'out'}' directive and a 'transition' directive`, });
attribute }
);
validator.error( validator.error(attribute, {
`An element can only have one '${hasIntro ? 'in' : 'out'}' directive`, code: `duplicate-transition`,
attribute message: `An element can only have one '${hasIntro ? 'in' : 'out'}' directive`
); });
} }
if (attribute.intro) hasIntro = true; if (attribute.intro) hasIntro = true;
@ -212,18 +224,18 @@ export default function validateElement(
if (bidi) hasTransition = true; if (bidi) hasTransition = true;
if (!validator.transitions.has(attribute.name)) { if (!validator.transitions.has(attribute.name)) {
validator.error( validator.error(attribute, {
`Missing transition '${attribute.name}'`, code: `missing-transition`,
attribute message: `Missing transition '${attribute.name}'`
); });
} }
} else if (attribute.type === 'Attribute') { } else if (attribute.type === 'Attribute') {
if (attribute.name === 'value' && node.name === 'textarea') { if (attribute.name === 'value' && node.name === 'textarea') {
if (node.children.length) { if (node.children.length) {
validator.error( validator.error(attribute, {
`A <textarea> can have either a value attribute or (equivalently) child content, but not both`, code: `textarea-duplicate-value`,
attribute message: `A <textarea> can have either a value attribute or (equivalently) child content, but not both`
); });
} }
} }
@ -232,16 +244,19 @@ export default function validateElement(
} }
} else if (attribute.type === 'Action') { } else if (attribute.type === 'Action') {
if (isComponent) { if (isComponent) {
validator.error(`Actions can only be applied to DOM elements, not components`, attribute); validator.error(attribute, {
code: `invalid-action`,
message: `Actions can only be applied to DOM elements, not components`
});
} }
validator.used.actions.add(attribute.name); validator.used.actions.add(attribute.name);
if (!validator.actions.has(attribute.name)) { if (!validator.actions.has(attribute.name)) {
validator.error( validator.error(attribute, {
`Missing action '${attribute.name}'`, code: `missing-action`,
attribute message: `Missing action '${attribute.name}'`
); });
} }
} }
}); });
@ -254,14 +269,17 @@ function checkTypeAttribute(validator: Validator, node: Node) {
if (!attribute) return null; if (!attribute) return null;
if (attribute.value === true) { if (attribute.value === true) {
validator.error(`'type' attribute must be specified`, attribute); validator.error(attribute, {
code: `missing-type`,
message: `'type' attribute must be specified`
});
} }
if (isDynamic(attribute)) { if (isDynamic(attribute)) {
validator.error( validator.error(attribute, {
`'type' attribute cannot be dynamic if input uses two-way binding`, code: `invalid-type`,
attribute message: `'type' attribute cannot be dynamic if input uses two-way binding`
); });
} }
return attribute.value[0].data; return attribute.value[0].data;
@ -269,10 +287,10 @@ function checkTypeAttribute(validator: Validator, node: Node) {
function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, stack: Node[]) { function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, stack: Node[]) {
if (isDynamic(attribute)) { if (isDynamic(attribute)) {
validator.error( validator.error(attribute, {
`slot attribute cannot have a dynamic value`, code: `invalid-slot-attribute`,
attribute message: `slot attribute cannot have a dynamic value`
); });
} }
let i = stack.length; let i = stack.length;
@ -286,11 +304,17 @@ function checkSlotAttribute(validator: Validator, node: Node, attribute: Node, s
if (parent.type === 'IfBlock' || parent.type === 'EachBlock') { if (parent.type === 'IfBlock' || parent.type === 'EachBlock') {
const message = `Cannot place slotted elements inside an ${parent.type === 'IfBlock' ? 'if' : 'each'}-block`; const message = `Cannot place slotted elements inside an ${parent.type === 'IfBlock' ? 'if' : 'each'}-block`;
validator.error(message, attribute); validator.error(attribute, {
code: `invalid-slotted-content`,
message
});
} }
} }
validator.error(`Element with a slot='...' attribute must be a descendant of a component or custom element`, attribute); validator.error(attribute, {
code: `invalid-slotted-content`,
message: `Element with a slot='...' attribute must be a descendant of a component or custom element`
});
} }
function isDynamic(attribute: Node) { function isDynamic(attribute: Node) {

@ -16,7 +16,10 @@ export default function validateEventHandlerCallee(
const { callee, type } = attribute.expression; const { callee, type } = attribute.expression;
if (type !== 'CallExpression') { if (type !== 'CallExpression') {
validator.error(`Expected a call expression`, attribute.expression); validator.error(attribute.expression, {
code: `invalid-event-handler`,
message: `Expected a call expression`
});
} }
const { name } = flattenReference(callee); const { name } = flattenReference(callee);
@ -30,10 +33,10 @@ export default function validateEventHandlerCallee(
if (name === 'store' && attribute.expression.callee.type === 'MemberExpression') { if (name === 'store' && attribute.expression.callee.type === 'MemberExpression') {
if (!validator.options.store) { if (!validator.options.store) {
validator.warn( validator.warn(attribute.expression, {
'compile with `store: true` in order to call store methods', code: `options-missing-store`,
attribute.expression message: 'compile with `store: true` in order to call store methods'
); });
} }
return; return;
} }
@ -59,5 +62,8 @@ export default function validateEventHandlerCallee(
message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`; message += `. '${callee.name}' exists on 'helpers', did you put it in the wrong place?`;
} }
validator.warn(message, attribute.expression); validator.warn(attribute.expression, {
code: `invalid-callee`,
message
});
} }

@ -4,7 +4,10 @@ import { Node } from '../../interfaces';
export default function validateHead(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) { export default function validateHead(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {
if (node.attributes.length) { if (node.attributes.length) {
validator.error(`<:Head> should not have any attributes or directives`, node); validator.error(node.attributes[0], {
code: `invalid-attribute`,
message: `<:Head> should not have any attributes or directives`
});
} }
// TODO ensure only valid elements are included here // TODO ensure only valid elements are included here

@ -21,12 +21,10 @@ export default function validateWindow(validator: Validator, node: Node, refs: M
if (attribute.value.type !== 'Identifier') { if (attribute.value.type !== 'Identifier') {
const { parts } = flattenReference(attribute.value); const { parts } = flattenReference(attribute.value);
validator.error( validator.error(attribute.value, {
`Bindings on <:Window/> must be to top-level properties, e.g. '${parts[ code: `invalid-binding`,
parts.length - 1 message: `Bindings on <:Window/> must be to top-level properties, e.g. '${parts[parts.length - 1]}' rather than '${parts.join('.')}'`
]}' rather than '${parts.join('.')}'`, });
attribute.value
);
} }
if (!~validBindings.indexOf(attribute.name)) { if (!~validBindings.indexOf(attribute.name)) {
@ -39,15 +37,15 @@ export default function validateWindow(validator: Validator, node: Node, refs: M
const message = `'${attribute.name}' is not a valid binding on <:Window>`; const message = `'${attribute.name}' is not a valid binding on <:Window>`;
if (match) { if (match) {
validator.error( validator.error(attribute, {
`${message} (did you mean '${match}'?)`, code: `invalid-binding`,
attribute message: `${message} (did you mean '${match}'?)`
); });
} else { } else {
validator.error( validator.error(attribute, {
`${message} — valid bindings are ${list(validBindings)}`, code: `invalid-binding`,
attribute message: `${message} — valid bindings are ${list(validBindings)}`
); });
} }
} }
} else if (attribute.type === 'EventHandler') { } else if (attribute.type === 'EventHandler') {

@ -59,9 +59,10 @@ export class Validator {
}; };
} }
error(message: string, pos: { start: number, end: number }) { error(pos: { start: number, end: number }, { code, message } : { code: string, message: string }) {
error(message, { error(message, {
name: 'ValidationError', name: 'ValidationError',
code,
source: this.source, source: this.source,
start: pos.start, start: pos.start,
end: pos.end, end: pos.end,
@ -69,7 +70,7 @@ export class Validator {
}); });
} }
warn(message: string, pos: { start: number, end: number }) { warn(pos: { start: number, end: number }, { code, message }: { code: string, message: string }) {
if (!this.locator) this.locator = getLocator(this.source); if (!this.locator) this.locator = getLocator(this.source);
const start = this.locator(pos.start); const start = this.locator(pos.start);
const end = this.locator(pos.end); const end = this.locator(pos.end);
@ -77,6 +78,7 @@ export class Validator {
const frame = getCodeFrame(this.source, start.line, start.column); const frame = getCodeFrame(this.source, start.line, start.column);
this.onwarn({ this.onwarn({
code,
message, message,
frame, frame,
loc: { line: start.line + 1, column: start.column }, loc: { line: start.line + 1, column: start.column },
@ -105,6 +107,7 @@ export default function validate(
if (name && /^[a-z]/.test(name)) { if (name && /^[a-z]/.test(name)) {
const message = `options.name should be capitalised`; const message = `options.name should be capitalised`;
onwarn({ onwarn({
code: `options-lowercase-name`,
message, message,
filename, filename,
toString: () => message, toString: () => message,
@ -148,10 +151,10 @@ export default function validate(
definitions.value.properties.forEach(prop => { definitions.value.properties.forEach(prop => {
const { name } = prop.key; const { name } = prop.key;
if (!validator.used[category].has(name)) { if (!validator.used[category].has(name)) {
validator.warn( validator.warn(prop, {
`The '${name}' ${categories[category]} is unused`, code: `unused-${category.slice(0, -1)}`,
prop message: `The '${name}' ${categories[category]} is unused`
); });
} }
}); });
} }

@ -14,15 +14,18 @@ export default function validateJs(validator: Validator, js: Node) {
js.content.body.forEach((node: Node) => { js.content.body.forEach((node: Node) => {
// check there are no named exports // check there are no named exports
if (node.type === 'ExportNamedDeclaration') { if (node.type === 'ExportNamedDeclaration') {
validator.error(`A component can only have a default export`, node); validator.error(node, {
code: `named-export`,
message: `A component can only have a default export`
});
} }
if (node.type === 'ExportDefaultDeclaration') { if (node.type === 'ExportDefaultDeclaration') {
if (node.declaration.type !== 'ObjectExpression') { if (node.declaration.type !== 'ObjectExpression') {
return validator.error( validator.error(node.declaration, {
`Default export must be an object literal`, code: `invalid-default-export`,
node.declaration message: `Default export must be an object literal`
); });
} }
checkForComputedKeys(validator, node.declaration.properties); checkForComputedKeys(validator, node.declaration.properties);
@ -36,17 +39,17 @@ export default function validateJs(validator: Validator, js: Node) {
// Remove these checks in version 2 // Remove these checks in version 2
if (props.has('oncreate') && props.has('onrender')) { if (props.has('oncreate') && props.has('onrender')) {
validator.error( validator.error(props.get('onrender'), {
'Cannot have both oncreate and onrender', code: `duplicate-oncreate`,
props.get('onrender') message: 'Cannot have both oncreate and onrender'
); });
} }
if (props.has('ondestroy') && props.has('onteardown')) { if (props.has('ondestroy') && props.has('onteardown')) {
validator.error( validator.error(props.get('onteardown'), {
'Cannot have both ondestroy and onteardown', code: `duplicate-ondestroy`,
props.get('onteardown') message: 'Cannot have both ondestroy and onteardown'
); });
} }
// ensure all exported props are valid // ensure all exported props are valid
@ -59,20 +62,20 @@ export default function validateJs(validator: Validator, js: Node) {
} else { } else {
const match = fuzzymatch(name, validPropList); const match = fuzzymatch(name, validPropList);
if (match) { if (match) {
validator.error( validator.error(prop, {
`Unexpected property '${name}' (did you mean '${match}'?)`, code: `unexpected-property`,
prop message: `Unexpected property '${name}' (did you mean '${match}'?)`
); });
} else if (/FunctionExpression/.test(prop.value.type)) { } else if (/FunctionExpression/.test(prop.value.type)) {
validator.error( validator.error(prop, {
`Unexpected property '${name}' (did you mean to include it in 'methods'?)`, code: `unexpected-property`,
prop message: `Unexpected property '${name}' (did you mean to include it in 'methods'?)`
); });
} else { } else {
validator.error( validator.error(prop, {
`Unexpected property '${name}'`, code: `unexpected-property`,
prop message: `Unexpected property '${name}'`
); });
} }
} }
}); });

@ -1,14 +1,14 @@
import checkForDupes from '../utils/checkForDupes'; import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys'; import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function actions(validator: Validator, prop: Node) { export default function actions(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') { if (prop.value.type !== 'ObjectExpression') {
validator.error( validator.error(prop, {
`The 'actions' property must be an object literal`, code: `invalid-actions`,
prop message: `The 'actions' property must be an object literal`
); });
} }
checkForDupes(validator, prop.value.properties); checkForDupes(validator, prop.value.properties);

@ -1,15 +1,15 @@
import checkForDupes from '../utils/checkForDupes'; import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys'; import checkForComputedKeys from '../utils/checkForComputedKeys';
import getName from '../../../utils/getName'; import getName from '../../../utils/getName';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function components(validator: Validator, prop: Node) { export default function components(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') { if (prop.value.type !== 'ObjectExpression') {
validator.error( validator.error(prop, {
`The 'components' property must be an object literal`, code: `invalid-components-property`,
prop message: `The 'components' property must be an object literal`
); });
} }
checkForDupes(validator, prop.value.properties); checkForDupes(validator, prop.value.properties);
@ -19,14 +19,18 @@ export default function components(validator: Validator, prop: Node) {
const name = getName(component.key); const name = getName(component.key);
if (name === 'state') { if (name === 'state') {
validator.error( // TODO is this still true?
`Component constructors cannot be called 'state' due to technical limitations`, validator.error(component, {
component code: `invalid-name`,
); message: `Component constructors cannot be called 'state' due to technical limitations`
});
} }
if (!/^[A-Z]/.test(name)) { if (!/^[A-Z]/.test(name)) {
validator.warn(`Component names should be capitalised`, component); validator.warn(component, {
code: `component-lowercase`,
message: `Component names should be capitalised`
});
} }
}); });
} }

@ -3,7 +3,7 @@ import checkForComputedKeys from '../utils/checkForComputedKeys';
import getName from '../../../utils/getName'; import getName from '../../../utils/getName';
import isValidIdentifier from '../../../utils/isValidIdentifier'; import isValidIdentifier from '../../../utils/isValidIdentifier';
import reservedNames from '../../../utils/reservedNames'; import reservedNames from '../../../utils/reservedNames';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope'; import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../utils/isThisGetCallExpression'; import isThisGetCallExpression from '../../../utils/isThisGetCallExpression';
@ -15,10 +15,10 @@ const isFunctionExpression = new Set([
export default function computed(validator: Validator, prop: Node) { export default function computed(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') { if (prop.value.type !== 'ObjectExpression') {
validator.error( validator.error(prop, {
`The 'computed' property must be an object literal`, code: `invalid-computed-property`,
prop message: `The 'computed' property must be an object literal`
); });
} }
checkForDupes(validator, prop.value.properties); checkForDupes(validator, prop.value.properties);
@ -29,49 +29,49 @@ export default function computed(validator: Validator, prop: Node) {
if (!isValidIdentifier(name)) { if (!isValidIdentifier(name)) {
const suggestion = name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&'); const suggestion = name.replace(/[^_$a-z0-9]/ig, '_').replace(/^\d/, '_$&');
validator.error( validator.error(computation, {
`Computed property name '${name}' is invalid — must be a valid identifier such as ${suggestion}`, code: `invalid-computed-name`,
computation message: `Computed property name '${name}' is invalid — must be a valid identifier such as ${suggestion}`
); });
} }
if (reservedNames.has(name)) { if (reservedNames.has(name)) {
validator.error( validator.error(computation, {
`Computed property name '${name}' is invalid — cannot be a JavaScript reserved word`, code: `invalid-computed-name`,
computation message: `Computed property name '${name}' is invalid — cannot be a JavaScript reserved word`
); });
} }
if (!isFunctionExpression.has(computation.value.type)) { if (!isFunctionExpression.has(computation.value.type)) {
validator.error( validator.error(computation.value, {
`Computed properties can be function expressions or arrow function expressions`, code: `invalid-computed-value`,
computation.value message: `Computed properties can be function expressions or arrow function expressions`
); });
} }
const { body, params } = computation.value; const { body, params } = computation.value;
walkThroughTopFunctionScope(body, (node: Node) => { walkThroughTopFunctionScope(body, (node: Node) => {
if (isThisGetCallExpression(node) && !node.callee.property.computed) { if (isThisGetCallExpression(node) && !node.callee.property.computed) {
validator.error( validator.error(node, {
`Cannot use this.get(...) — values must be passed into the function as arguments`, code: `impure-computed`,
node message: `Cannot use this.get(...) — values must be passed into the function as arguments`
); });
} }
if (node.type === 'ThisExpression') { if (node.type === 'ThisExpression') {
validator.error( validator.error(node, {
`Computed properties should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`, code: `impure-computed`,
node message: `Computed properties should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`
); });
} }
}); });
if (params.length === 0) { if (params.length === 0) {
validator.error( validator.error(computation.value, {
`A computed value must depend on at least one property`, code: `impure-computed`,
computation.value message: `A computed value must depend on at least one property`
); });
} }
params.forEach((param: Node) => { params.forEach((param: Node) => {
@ -81,10 +81,11 @@ export default function computed(validator: Validator, prop: Node) {
param.left.type === 'Identifier'); param.left.type === 'Identifier');
if (!valid) { if (!valid) {
validator.error( // TODO change this for v2
`Computed properties cannot use destructuring in function parameters`, validator.error(param, {
param code: `invalid-computed-arguments`,
); message: `Computed properties cannot use destructuring in function parameters`
});
} }
}); });
}); });

@ -1,4 +1,4 @@
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']); const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']);
@ -7,6 +7,9 @@ export default function data(validator: Validator, prop: Node) {
while (prop.type === 'ParenthesizedExpression') prop = prop.expression; while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
if (disallowed.has(prop.value.type)) { if (disallowed.has(prop.value.type)) {
validator.error(`'data' must be a function`, prop.value); validator.error(prop.value, {
code: `invalid-data-property`,
message: `'data' must be a function`
});
} }
} }

@ -1,14 +1,14 @@
import checkForDupes from '../utils/checkForDupes'; import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys'; import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function events(validator: Validator, prop: Node) { export default function events(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') { if (prop.value.type !== 'ObjectExpression') {
validator.error( validator.error(prop, {
`The 'events' property must be an object literal`, code: `invalid-events-property`,
prop message: `The 'events' property must be an object literal`
); });
} }
checkForDupes(validator, prop.value.properties); checkForDupes(validator, prop.value.properties);

@ -1,17 +1,17 @@
import checkForDupes from '../utils/checkForDupes'; import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys'; import checkForComputedKeys from '../utils/checkForComputedKeys';
import { walk } from 'estree-walker'; import { walk } from 'estree-walker';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope'; import walkThroughTopFunctionScope from '../../../utils/walkThroughTopFunctionScope';
import isThisGetCallExpression from '../../../utils/isThisGetCallExpression'; import isThisGetCallExpression from '../../../utils/isThisGetCallExpression';
export default function helpers(validator: Validator, prop: Node) { export default function helpers(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') { if (prop.value.type !== 'ObjectExpression') {
validator.error( validator.error(prop, {
`The 'helpers' property must be an object literal`, code: `invalid-helpers-property`,
prop message: `The 'helpers' property must be an object literal`
); });
} }
checkForDupes(validator, prop.value.properties); checkForDupes(validator, prop.value.properties);
@ -24,27 +24,27 @@ export default function helpers(validator: Validator, prop: Node) {
walkThroughTopFunctionScope(prop.value.body, (node: Node) => { walkThroughTopFunctionScope(prop.value.body, (node: Node) => {
if (isThisGetCallExpression(node) && !node.callee.property.computed) { if (isThisGetCallExpression(node) && !node.callee.property.computed) {
validator.error( validator.error(node, {
`Cannot use this.get(...) — values must be passed into the helper function as arguments`, code: `impure-helper`,
node message: `Cannot use this.get(...) — values must be passed into the helper function as arguments`
); });
} }
if (node.type === 'ThisExpression') { if (node.type === 'ThisExpression') {
validator.error( validator.error(node, {
`Helpers should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`, code: `impure-helper`,
node message: `Helpers should be pure functions — they do not have access to the component instance and cannot use 'this'. Did you mean to put this in 'methods'?`
); });
} else if (node.type === 'Identifier' && node.name === 'arguments') { } else if (node.type === 'Identifier' && node.name === 'arguments') {
usesArguments = true; usesArguments = true;
} }
}); });
if (prop.value.params.length === 0 && !usesArguments) { if (prop.value.params.length === 0 && !usesArguments) {
validator.warn( validator.warn(prop, {
`Helpers should be pure functions, with at least one argument`, code: `impure-helper`,
prop message: `Helpers should be pure functions, with at least one argument`
); });
} }
}); });
} }

@ -1,11 +1,11 @@
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function immutable(validator: Validator, prop: Node) { export default function immutable(validator: Validator, prop: Node) {
if (prop.value.type !== 'Literal' || typeof prop.value.value !== 'boolean') { if (prop.value.type !== 'Literal' || typeof prop.value.value !== 'boolean') {
validator.error( validator.error(prop.value, {
`'immutable' must be a boolean literal`, code: `invalid-immutable-property`,
prop.value message: `'immutable' must be a boolean literal`
); });
} }
} }

@ -3,17 +3,17 @@ import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys'; import checkForComputedKeys from '../utils/checkForComputedKeys';
import usesThisOrArguments from '../utils/usesThisOrArguments'; import usesThisOrArguments from '../utils/usesThisOrArguments';
import getName from '../../../utils/getName'; import getName from '../../../utils/getName';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
const builtin = new Set(['set', 'get', 'on', 'fire', 'observe', 'destroy']); const builtin = new Set(['set', 'get', 'on', 'fire', 'observe', 'destroy']);
export default function methods(validator: Validator, prop: Node) { export default function methods(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') { if (prop.value.type !== 'ObjectExpression') {
validator.error( validator.error(prop, {
`The 'methods' property must be an object literal`, code: `invalid-methods-property`,
prop message: `The 'methods' property must be an object literal`
); });
} }
checkForAccessors(validator, prop.value.properties, 'Methods'); checkForAccessors(validator, prop.value.properties, 'Methods');
@ -24,19 +24,18 @@ export default function methods(validator: Validator, prop: Node) {
const name = getName(prop.key); const name = getName(prop.key);
if (builtin.has(name)) { if (builtin.has(name)) {
validator.error( validator.error(prop, {
`Cannot overwrite built-in method '${name}'`, code: `invalid-method-name`,
prop message: `Cannot overwrite built-in method '${name}'`
); });
} }
if (prop.value.type === 'ArrowFunctionExpression') { if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) { if (usesThisOrArguments(prop.value.body)) {
validator.error( validator.error(prop, {
`Method '${prop.key code: `invalid-method-value`,
.name}' should be a function expression, not an arrow function expression`, message: `Method '${prop.key.name}' should be a function expression, not an arrow function expression`
prop });
);
} }
} }
}); });

@ -1,7 +1,7 @@
import * as namespaces from '../../../utils/namespaces'; import * as namespaces from '../../../utils/namespaces';
import nodeToString from '../../../utils/nodeToString' import nodeToString from '../../../utils/nodeToString'
import fuzzymatch from '../../utils/fuzzymatch'; import fuzzymatch from '../../utils/fuzzymatch';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
const valid = new Set(namespaces.validNamespaces); const valid = new Set(namespaces.validNamespaces);
@ -10,21 +10,24 @@ export default function namespace(validator: Validator, prop: Node) {
const ns = nodeToString(prop.value); const ns = nodeToString(prop.value);
if (typeof ns !== 'string') { if (typeof ns !== 'string') {
validator.error( validator.error(prop, {
`The 'namespace' property must be a string literal representing a valid namespace`, code: `invalid-namespace-property`,
prop message: `The 'namespace' property must be a string literal representing a valid namespace`
); });
} }
if (!valid.has(ns)) { if (!valid.has(ns)) {
const match = fuzzymatch(ns, namespaces.validNamespaces); const match = fuzzymatch(ns, namespaces.validNamespaces);
if (match) { if (match) {
validator.error( validator.error(prop, {
`Invalid namespace '${ns}' (did you mean '${match}'?)`, code: `invalid-namespace-property`,
prop message: `Invalid namespace '${ns}' (did you mean '${match}'?)`
); });
} else { } else {
validator.error(`Invalid namespace '${ns}'`, prop); validator.error(prop, {
code: `invalid-namespace-property`,
message: `Invalid namespace '${ns}'`
});
} }
} }
} }

@ -1,14 +1,14 @@
import usesThisOrArguments from '../utils/usesThisOrArguments'; import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function oncreate(validator: Validator, prop: Node) { export default function oncreate(validator: Validator, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') { if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) { if (usesThisOrArguments(prop.value.body)) {
validator.error( validator.error(prop, {
`'oncreate' should be a function expression, not an arrow function expression`, code: `invalid-oncreate-property`,
prop message: `'oncreate' should be a function expression, not an arrow function expression`
); });
} }
} }
} }

@ -1,14 +1,14 @@
import usesThisOrArguments from '../utils/usesThisOrArguments'; import usesThisOrArguments from '../utils/usesThisOrArguments';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function ondestroy(validator: Validator, prop: Node) { export default function ondestroy(validator: Validator, prop: Node) {
if (prop.value.type === 'ArrowFunctionExpression') { if (prop.value.type === 'ArrowFunctionExpression') {
if (usesThisOrArguments(prop.value.body)) { if (usesThisOrArguments(prop.value.body)) {
validator.error( validator.error(prop, {
`'ondestroy' should be a function expression, not an arrow function expression`, code: `invalid-ondestroy-property`,
prop message: `'ondestroy' should be a function expression, not an arrow function expression`
); });
} }
} }
} }

@ -1,11 +1,12 @@
import oncreate from './oncreate'; import oncreate from './oncreate';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function onrender(validator: Validator, prop: Node) { export default function onrender(validator: Validator, prop: Node) {
validator.warn( validator.warn(prop, {
`'onrender' has been deprecated in favour of 'oncreate', and will cause an error in Svelte 2.x`, code: `deprecated-onrender`,
prop message: `'onrender' has been deprecated in favour of 'oncreate', and will cause an error in Svelte 2.x`
); });
oncreate(validator, prop); oncreate(validator, prop);
} }

@ -1,11 +1,12 @@
import ondestroy from './ondestroy'; import ondestroy from './ondestroy';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function onteardown(validator: Validator, prop: Node) { export default function onteardown(validator: Validator, prop: Node) {
validator.warn( validator.warn(prop, {
`'onteardown' has been deprecated in favour of 'ondestroy', and will cause an error in Svelte 2.x`, code: `deprecated-onteardown`,
prop message: `'onteardown' has been deprecated in favour of 'ondestroy', and will cause an error in Svelte 2.x`
); });
ondestroy(validator, prop); ondestroy(validator, prop);
} }

@ -1,4 +1,4 @@
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function preload(validator: Validator, prop: Node) { export default function preload(validator: Validator, prop: Node) {

@ -1,21 +1,21 @@
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import nodeToString from '../../../utils/nodeToString'; import nodeToString from '../../../utils/nodeToString';
export default function props(validator: Validator, prop: Node) { export default function props(validator: Validator, prop: Node) {
if (prop.value.type !== 'ArrayExpression') { if (prop.value.type !== 'ArrayExpression') {
validator.error( validator.error(prop.value, {
`'props' must be an array expression, if specified`, code: `invalid-props-property`,
prop.value message: `'props' must be an array expression, if specified`
); });
} }
prop.value.elements.forEach((element: Node) => { prop.value.elements.forEach((element: Node) => {
if (typeof nodeToString(element) !== 'string') { if (typeof nodeToString(element) !== 'string') {
validator.error( validator.error(element, {
`'props' must be an array of string literals`, code: `invalid-props-property`,
element message: `'props' must be an array of string literals`
); });
} }
}); });
} }

@ -1,4 +1,4 @@
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']); const disallowed = new Set(['Literal', 'ObjectExpression', 'ArrayExpression']);
@ -7,6 +7,9 @@ export default function setup(validator: Validator, prop: Node) {
while (prop.type === 'ParenthesizedExpression') prop = prop.expression; while (prop.type === 'ParenthesizedExpression') prop = prop.expression;
if (disallowed.has(prop.value.type)) { if (disallowed.has(prop.value.type)) {
validator.error(`'setup' must be a function`, prop.value); validator.error(prop.value, {
code: `invalid-setup-property`,
message: `'setup' must be a function`
});
} }
} }

@ -1,4 +1,4 @@
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function store(validator: Validator, prop: Node) { export default function store(validator: Validator, prop: Node) {

@ -1,20 +1,20 @@
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import nodeToString from '../../../utils/nodeToString'; import nodeToString from '../../../utils/nodeToString';
export default function tag(validator: Validator, prop: Node) { export default function tag(validator: Validator, prop: Node) {
const tag = nodeToString(prop.value); const tag = nodeToString(prop.value);
if (typeof tag !== 'string') { if (typeof tag !== 'string') {
validator.error( validator.error(prop.value, {
`'tag' must be a string literal`, code: `invalid-tag-property`,
prop.value message: `'tag' must be a string literal`
); });
} }
if (!/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) { if (!/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
validator.error( validator.error(prop.value, {
`tag name must be two or more words joined by the '-' character`, code: `invalid-tag-property`,
prop.value message: `tag name must be two or more words joined by the '-' character`
); });
} }
} }

@ -1,14 +1,14 @@
import checkForDupes from '../utils/checkForDupes'; import checkForDupes from '../utils/checkForDupes';
import checkForComputedKeys from '../utils/checkForComputedKeys'; import checkForComputedKeys from '../utils/checkForComputedKeys';
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function transitions(validator: Validator, prop: Node) { export default function transitions(validator: Validator, prop: Node) {
if (prop.value.type !== 'ObjectExpression') { if (prop.value.type !== 'ObjectExpression') {
validator.error( validator.error(prop, {
`The 'transitions' property must be an object literal`, code: `invalid-transitions-property`,
prop message: `The 'transitions' property must be an object literal`
); });
} }
checkForDupes(validator, prop.value.properties); checkForDupes(validator, prop.value.properties);

@ -1,4 +1,4 @@
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function checkForAccessors( export default function checkForAccessors(
@ -8,7 +8,10 @@ export default function checkForAccessors(
) { ) {
properties.forEach(prop => { properties.forEach(prop => {
if (prop.kind !== 'init') { if (prop.kind !== 'init') {
validator.error(`${label} cannot use getters and setters`, prop); validator.error(prop, {
code: `illegal-accessor`,
message: `${label} cannot use getters and setters`
});
} }
}); });
} }

@ -1,4 +1,4 @@
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
export default function checkForComputedKeys( export default function checkForComputedKeys(
@ -7,7 +7,10 @@ export default function checkForComputedKeys(
) { ) {
properties.forEach(prop => { properties.forEach(prop => {
if (prop.key.computed) { if (prop.key.computed) {
validator.error(`Cannot use computed keys`, prop); validator.error(prop, {
code: `computed-key`,
message: `Cannot use computed keys`
});
} }
}); });
} }

@ -1,4 +1,4 @@
import { Validator } from '../../'; import { Validator } from '../../index';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import getName from '../../../utils/getName'; import getName from '../../../utils/getName';
@ -12,7 +12,10 @@ export default function checkForDupes(
const name = getName(prop.key); const name = getName(prop.key);
if (seen.has(name)) { if (seen.has(name)) {
validator.error(`Duplicate property '${name}'`, prop); validator.error(prop, {
code: `duplicate-property`,
message: `Duplicate property '${name}'`
});
} }
seen.add(name); seen.add(name);

@ -3,6 +3,7 @@ export default {
warnings: [{ warnings: [{
filename: "SvelteComponent.html", filename: "SvelteComponent.html",
code: `css-unused-selector`,
message: "Unused CSS selector", message: "Unused CSS selector",
loc: { loc: {
line: 4, line: 4,

@ -2,6 +2,7 @@ export default {
cascade: false, cascade: false,
warnings: [{ warnings: [{
code: `css-unused-selector`,
message: 'Unused CSS selector', message: 'Unused CSS selector',
loc: { loc: {
line: 8, line: 8,

@ -2,6 +2,7 @@ export default {
cascade: false, cascade: false,
warnings: [{ warnings: [{
code: 'missing-component',
message: 'P component is not defined', message: 'P component is not defined',
loc: { loc: {
line: 2, line: 2,

@ -6,6 +6,7 @@ export default {
}, },
warnings: [{ warnings: [{
code: `css-unused-selector`,
message: 'Unused CSS selector', message: 'Unused CSS selector',
loc: { loc: {
column: 1, column: 1,

@ -2,6 +2,7 @@ export default {
cascade: false, cascade: false,
warnings: [{ warnings: [{
code: `css-unused-selector`,
message: 'Unused CSS selector', message: 'Unused CSS selector',
loc: { loc: {
column: 1, column: 1,

@ -4,6 +4,7 @@ export default {
warnings: [ warnings: [
{ {
filename: "SvelteComponent.html", filename: "SvelteComponent.html",
code: `css-unused-selector`,
message: "Unused CSS selector", message: "Unused CSS selector",
loc: { loc: {
line: 4, line: 4,
@ -21,6 +22,7 @@ export default {
{ {
filename: "SvelteComponent.html", filename: "SvelteComponent.html",
code: `css-unused-selector`,
message: "Unused CSS selector", message: "Unused CSS selector",
loc: { loc: {
line: 4, line: 4,

@ -7,6 +7,7 @@ export default {
warnings: [{ warnings: [{
filename: "SvelteComponent.html", filename: "SvelteComponent.html",
code: `css-unused-selector`,
message: "Unused CSS selector", message: "Unused CSS selector",
loc: { loc: {
line: 12, line: 12,

@ -3,6 +3,7 @@ export default {
warnings: [{ warnings: [{
filename: "SvelteComponent.html", filename: "SvelteComponent.html",
code: `css-unused-selector`,
message: "Unused CSS selector", message: "Unused CSS selector",
loc: { loc: {
line: 8, line: 8,

@ -184,6 +184,7 @@ describe("formats", () => {
); );
assert.deepEqual(warnings, [{ assert.deepEqual(warnings, [{
code: `options-missing-globals`,
message: `No name was supplied for imported module 'lodash'. Guessing '_', but you should use options.globals` message: `No name was supplied for imported module 'lodash'. Guessing '_', but you should use options.globals`
}]); }]);
}); });

@ -32,6 +32,7 @@ describe('parse', () => {
if (!expectedError) throw err; if (!expectedError) throw err;
try { try {
assert.equal(err.code, expectedError.code);
assert.equal(err.message, expectedError.message); assert.equal(err.message, expectedError.message);
assert.deepEqual(err.loc, expectedError.loc); assert.deepEqual(err.loc, expectedError.loc);
assert.equal(err.pos, expectedError.pos); assert.equal(err.pos, expectedError.pos);

@ -1,4 +1,5 @@
{ {
"code": "duplicate-attribute",
"message": "Attributes need to be unique", "message": "Attributes need to be unique",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "duplicate-attribute",
"message": "Attributes need to be unique", "message": "Attributes need to be unique",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "binding-disabled",
"message": "Two-way binding is disabled", "message": "Two-way binding is disabled",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "binding-disabled",
"message": "Two-way binding is disabled", "message": "Two-way binding is disabled",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-directive-value",
"message": "directive values should not be wrapped — use 'foo', not '{foo}'", "message": "directive values should not be wrapped — use 'foo', not '{foo}'",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-directive-value",
"message": "directive values should not be wrapped — use 'foo', not '{{foo}}'", "message": "directive values should not be wrapped — use 'foo', not '{{foo}}'",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-directive-value",
"message": "Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)", "message": "Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)",
"pos": 19, "pos": 19,
"loc": { "loc": {

@ -1,4 +1,5 @@
{ {
"code": "invalid-directive-value",
"message": "Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)", "message": "Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)",
"pos": 19, "pos": 19,
"loc": { "loc": {

@ -1,4 +1,5 @@
{ {
"code": "unexpected-eof",
"message": "comment was left open, expected -->", "message": "comment was left open, expected -->",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "unexpected-eof",
"message": "comment was left open, expected -->", "message": "comment was left open, expected -->",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "css-syntax-error",
"message": "LeftCurlyBracket is expected", "message": "LeftCurlyBracket is expected",
"loc": { "loc": {
"line": 2, "line": 2,

@ -1,4 +1,5 @@
{ {
"code": "css-syntax-error",
"message": "LeftCurlyBracket is expected", "message": "LeftCurlyBracket is expected",
"loc": { "loc": {
"line": 2, "line": 2,

@ -1,4 +1,5 @@
{ {
"code": "invalid-directive-value",
"message": "Expected a method call", "message": "Expected a method call",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-directive-value",
"message": "Expected a method call", "message": "Expected a method call",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "parse-error",
"message": "Assigning to rvalue", "message": "Assigning to rvalue",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "parse-error",
"message": "Assigning to rvalue", "message": "Assigning to rvalue",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "duplicate-style",
"message": "You can only have one top-level <style> tag per component", "message": "You can only have one top-level <style> tag per component",
"loc": { "loc": {
"line": 9, "line": 9,

@ -1,4 +1,5 @@
{ {
"code": "duplicate-style",
"message": "You can only have one top-level <style> tag per component", "message": "You can only have one top-level <style> tag per component",
"loc": { "loc": {
"line": 9, "line": 9,

@ -1,4 +1,5 @@
{ {
"code": "invalid-directive-value",
"message": "ref directives cannot have a value", "message": "ref directives cannot have a value",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-directive-value",
"message": "ref directives cannot have a value", "message": "ref directives cannot have a value",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "unclosed-script",
"message": "<script> must have a closing tag", "message": "<script> must have a closing tag",
"loc": { "loc": {
"line": 3, "line": 3,

@ -1,4 +1,5 @@
{ {
"code": "unclosed-script",
"message": "<script> must have a closing tag", "message": "<script> must have a closing tag",
"loc": { "loc": {
"line": 3, "line": 3,

@ -1,4 +1,5 @@
{ {
"code": "invalid-self-placement",
"message": "<svelte:self> components can only exist inside if-blocks or each-blocks", "message": "<svelte:self> components can only exist inside if-blocks or each-blocks",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-self-placement",
"message": "<:Self> components can only exist inside if-blocks or each-blocks", "message": "<:Self> components can only exist inside if-blocks or each-blocks",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "unexpected-eof",
"message": "Unexpected end of input", "message": "Unexpected end of input",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "unexpected-eof",
"message": "Unexpected end of input", "message": "Unexpected end of input",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "unexpected-eof",
"message": "Unexpected end of input", "message": "Unexpected end of input",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "unexpected-eof",
"message": "Unexpected end of input", "message": "Unexpected end of input",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "unclosed-block",
"message": "Block was left open", "message": "Block was left open",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "unclosed-block",
"message": "Block was left open", "message": "Block was left open",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "unclosed-element",
"message": "<div> was left open", "message": "<div> was left open",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "unclosed-element",
"message": "<div> was left open", "message": "<div> was left open",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-closing-tag",
"message": "</div> attempted to close an element that was not open", "message": "</div> attempted to close an element that was not open",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-closing-tag",
"message": "</div> attempted to close an element that was not open", "message": "</div> attempted to close an element that was not open",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-void-content",
"message": "<input> is a void element and cannot have children, or a closing tag", "message": "<input> is a void element and cannot have children, or a closing tag",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-void-content",
"message": "<input> is a void element and cannot have children, or a closing tag", "message": "<input> is a void element and cannot have children, or a closing tag",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-window-content",
"message": "<svelte:window> cannot have children", "message": "<svelte:window> cannot have children",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "invalid-window-content",
"message": "<:Window> cannot have children", "message": "<:Window> cannot have children",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,4 +1,5 @@
{ {
"code": "duplicate-window",
"message": "A component can only have one <svelte:window> tag", "message": "A component can only have one <svelte:window> tag",
"loc": { "loc": {
"line": 2, "line": 2,

@ -1,4 +1,5 @@
{ {
"code": "duplicate-window",
"message": "A component can only have one <:Window> tag", "message": "A component can only have one <:Window> tag",
"loc": { "loc": {
"line": 2, "line": 2,

@ -1,4 +1,5 @@
{ {
"code": "invalid-window-placement",
"message": "<svelte:window> tags cannot be inside elements or blocks", "message": "<svelte:window> tags cannot be inside elements or blocks",
"loc": { "loc": {
"line": 2, "line": 2,

@ -1,4 +1,5 @@
{ {
"code": "invalid-window-placement",
"message": "<:Window> tags cannot be inside elements or blocks", "message": "<:Window> tags cannot be inside elements or blocks",
"loc": { "loc": {
"line": 2, "line": 2,

@ -1,4 +1,5 @@
{ {
"code": "invalid-window-placement",
"message": "<svelte:window> tags cannot be inside elements or blocks", "message": "<svelte:window> tags cannot be inside elements or blocks",
"loc": { "loc": {
"line": 2, "line": 2,

@ -1,4 +1,5 @@
{ {
"code": "invalid-window-placement",
"message": "<:Window> tags cannot be inside elements or blocks", "message": "<:Window> tags cannot be inside elements or blocks",
"loc": { "loc": {
"line": 2, "line": 2,

@ -25,12 +25,8 @@ describe("validate", () => {
const { stats } = svelte.compile(input, { const { stats } = svelte.compile(input, {
onwarn(warning) { onwarn(warning) {
warnings.push({ const { code, message, pos, loc, end } = warning;
message: warning.message, warnings.push({ code, message, pos, loc, end });
pos: warning.pos,
loc: warning.loc,
end: warning.end,
});
}, },
dev: config.dev dev: config.dev
}); });
@ -39,6 +35,7 @@ describe("validate", () => {
stats.warnings.forEach((full, i) => { stats.warnings.forEach((full, i) => {
const lite = warnings[i]; const lite = warnings[i];
assert.deepEqual({ assert.deepEqual({
code: full.code,
message: full.message, message: full.message,
pos: full.pos, pos: full.pos,
loc: full.loc, loc: full.loc,
@ -62,6 +59,7 @@ describe("validate", () => {
throw new Error(`Expected an error: ${expected.message}`); throw new Error(`Expected an error: ${expected.message}`);
} }
assert.equal(error.code, expected.code);
assert.equal(error.message, expected.message); assert.equal(error.message, expected.message);
assert.deepEqual(error.loc, expected.loc); assert.deepEqual(error.loc, expected.loc);
assert.deepEqual(error.end, expected.end); assert.deepEqual(error.end, expected.end);
@ -100,6 +98,7 @@ describe("validate", () => {
name: "lowercase", name: "lowercase",
onwarn(warning) { onwarn(warning) {
warnings.push({ warnings.push({
code: warning.code,
message: warning.message, message: warning.message,
pos: warning.pos, pos: warning.pos,
loc: warning.loc loc: warning.loc
@ -108,6 +107,7 @@ describe("validate", () => {
}); });
assert.deepEqual(warnings, [ assert.deepEqual(warnings, [
{ {
code: `options-lowercase-name`,
message: "options.name should be capitalised", message: "options.name should be capitalised",
pos: undefined, pos: undefined,
loc: undefined loc: undefined
@ -121,6 +121,7 @@ describe("validate", () => {
name: "_", name: "_",
onwarn(warning) { onwarn(warning) {
warnings.push({ warnings.push({
code: warning.code,
message: warning.message, message: warning.message,
pos: warning.pos, pos: warning.pos,
loc: warning.loc loc: warning.loc

@ -1,5 +1,6 @@
[ [
{ {
"code": "a11y-missing-attribute",
"message": "A11y: <img> element should have an alt attribute", "message": "A11y: <img> element should have an alt attribute",
"loc": { "loc": {
"line": 1, "line": 1,
@ -13,6 +14,7 @@
}, },
{ {
"code": "a11y-missing-attribute",
"message": "A11y: <area> element should have an alt, aria-label or aria-labelledby attribute", "message": "A11y: <area> element should have an alt, aria-label or aria-labelledby attribute",
"loc": { "loc": {
"line": 4, "line": 4,
@ -26,6 +28,7 @@
}, },
{ {
"code": "a11y-missing-attribute",
"message": "A11y: <object> element should have a title, aria-label or aria-labelledby attribute", "message": "A11y: <object> element should have a title, aria-label or aria-labelledby attribute",
"loc": { "loc": {
"line": 7, "line": 7,
@ -39,6 +42,7 @@
}, },
{ {
"code": "a11y-missing-attribute",
"message": "A11y: <input type=\"image\"> element should have an alt, aria-label or aria-labelledby attribute", "message": "A11y: <input type=\"image\"> element should have an alt, aria-label or aria-labelledby attribute",
"loc": { "loc": {
"line": 9, "line": 9,

@ -1,4 +1,5 @@
[{ [{
"code": "a11y-missing-content",
"message": "A11y: <a> element should have child content", "message": "A11y: <a> element should have child content",
"loc": { "loc": {
"line": 1, "line": 1,

@ -1,5 +1,6 @@
[ [
{ {
"code": "a11y-missing-attribute",
"message": "A11y: <a> element should have an href attribute", "message": "A11y: <a> element should have an href attribute",
"loc": { "loc": {
"line": 1, "line": 1,
@ -12,6 +13,7 @@
"pos": 11 "pos": 11
}, },
{ {
"code": "a11y-invalid-attribute",
"message": "A11y: '' is not a valid xlink:href attribute", "message": "A11y: '' is not a valid xlink:href attribute",
"loc": { "loc": {
"line": 2, "line": 2,
@ -24,6 +26,7 @@
"pos": 65 "pos": 65
}, },
{ {
"code": "a11y-invalid-attribute",
"message": "A11y: '#' is not a valid xlink:href attribute", "message": "A11y: '#' is not a valid xlink:href attribute",
"loc": { "loc": {
"line": 3, "line": 3,

@ -1,5 +1,6 @@
[ [
{ {
"code": "a11y-missing-attribute",
"message": "A11y: <a> element should have an href attribute", "message": "A11y: <a> element should have an href attribute",
"loc": { "loc": {
"line": 1, "line": 1,
@ -12,6 +13,7 @@
"pos": 0 "pos": 0
}, },
{ {
"code": "a11y-invalid-attribute",
"message": "A11y: '' is not a valid href attribute", "message": "A11y: '' is not a valid href attribute",
"loc": { "loc": {
"line": 2, "line": 2,
@ -24,6 +26,7 @@
"pos": 30 "pos": 30
}, },
{ {
"code": "a11y-invalid-attribute",
"message": "A11y: '#' is not a valid href attribute", "message": "A11y: '#' is not a valid href attribute",
"loc": { "loc": {
"line": 3, "line": 3,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save