diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 595cc12edb..6eff2f2a3d 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -1,5 +1,7 @@ import MagicString, { Bundle } from 'magic-string'; import { walk } from 'estree-walker'; +import { getLocator } from 'locate-character'; +import getCodeFrame from '../utils/getCodeFrame'; import isReference from '../utils/isReference'; import flattenReference from '../utils/flattenReference'; import globalWhitelist from '../utils/globalWhitelist'; @@ -619,4 +621,31 @@ export default class Generator { this.namespace = namespace; this.templateProperties = templateProperties; } + + warnOnUnusedSelectors() { + if (this.cascade) return; + + let locator; + + this.selectors.forEach((selector: Selector) => { + if (!selector.used) { + const pos = selector.node.start; + + if (!locator) locator = getLocator(this.source); + const { line, column } = locator(pos); + + const frame = getCodeFrame(this.source, line, column); + const message = `Unused CSS selector`; + + this.options.onwarn({ + message, + frame, + loc: { line: line + 1, column }, + pos, + filename: this.options.filename, + toString: () => `${message} (${line + 1}:${column})\n${frame}`, + }); + } + }); + } } diff --git a/src/generators/Selector.ts b/src/generators/Selector.ts index 4a1c566b69..862bbea650 100644 --- a/src/generators/Selector.ts +++ b/src/generators/Selector.ts @@ -1,14 +1,17 @@ -import { walkRules } from '../utils/css'; +import { groupSelectors, isGlobalSelector, walkRules } from '../utils/css'; import { Node } from '../interfaces'; export default class Selector { node: Node; + blocks: Node[][]; parts: Node[]; used: boolean; constructor(node: Node) { this.node = node; + this.blocks = groupSelectors(this.node); + // take trailing :global(...) selectors out of consideration let i = node.children.length; while (i > 2) { @@ -24,13 +27,15 @@ export default class Selector { this.parts = node.children.slice(0, i); - this.used = false; // TODO use this! warn on unused selectors + this.used = isGlobalSelector(this.blocks[0]); } apply(node: Node, stack: Node[]) { const applies = selectorAppliesTo(this.parts, node, stack.slice()); if (applies) { + this.used = true; + // add svelte-123xyz attribute to outermost and innermost // elements — no need to add it to intermediate elements node._needsCssAttribute = true; diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index 1035f83643..2810f2cbd1 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -61,6 +61,8 @@ export default function dom( const { block, state } = preprocess(generator, namespace, parsed.html); + generator.warnOnUnusedSelectors(); + parsed.html.children.forEach((node: Node) => { visit(generator, block, state, node, []); }); diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index 92c3599aeb..ff18b06878 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -27,6 +27,8 @@ export class SsrGenerator extends Generator { preprocess(this, parsed.html); + this.warnOnUnusedSelectors(); + if (templateProperties.oncreate) removeNode( this.code, diff --git a/src/interfaces.ts b/src/interfaces.ts index fbb745b7b8..78fc8730b9 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -26,9 +26,11 @@ export interface Parsed { } export interface Warning { - loc?: { line: number; column: number; pos: number }; + loc?: { line: number; column: number; pos?: number }; + pos?: number; message: string; filename?: string; + frame?: string; toString: () => string; } diff --git a/src/validate/index.ts b/src/validate/index.ts index 32a6a82c92..62cb9e614d 100644 --- a/src/validate/index.ts +++ b/src/validate/index.ts @@ -35,9 +35,9 @@ export class Validator { constructor(parsed: Parsed, source: string, options: CompileOptions) { this.source = source; - this.filename = options !== undefined ? options.filename : undefined; + this.filename = options.filename; - this.onwarn = options !== undefined ? options.onwarn : undefined; + this.onwarn = options.onwarn; this.namespace = null; this.defaultExport = null; diff --git a/test/css/index.js b/test/css/index.js index e4f31f00a3..221f679739 100644 --- a/test/css/index.js +++ b/test/css/index.js @@ -4,13 +4,21 @@ import { env, normalizeHtml, svelte } from "../helpers.js"; function tryRequire(file) { try { - return require(file).default; + const mod = require(file); + return mod.default || mod; } catch (err) { if (err.code !== "MODULE_NOT_FOUND") throw err; return null; } } +function normalizeWarning(warning) { + warning.frame = warning.frame.replace(/^\n/, '').replace(/^\t+/gm, ''); + delete warning.filename; + delete warning.toString; + return warning; +} + describe("css", () => { fs.readdirSync("test/css/samples").forEach(dir => { if (dir[0] === ".") return; @@ -28,19 +36,32 @@ describe("css", () => { .readFileSync(`test/css/samples/${dir}/input.html`, "utf-8") .replace(/\s+$/, ""); + const expectedWarnings = (config.warnings || []).map(normalizeWarning); + const domWarnings = []; + const ssrWarnings = []; + const dom = svelte.compile(input, Object.assign(config, { format: 'iife', - name: 'SvelteComponent' + name: 'SvelteComponent', + onwarn: warning => { + domWarnings.push(warning); + } })); const ssr = svelte.compile(input, Object.assign(config, { format: 'iife', generate: 'ssr', - name: 'SvelteComponent' + name: 'SvelteComponent', + onwarn: warning => { + ssrWarnings.push(warning); + } })); assert.equal(dom.css, ssr.css); + assert.deepEqual(domWarnings.map(normalizeWarning), ssrWarnings.map(normalizeWarning)); + assert.deepEqual(domWarnings.map(normalizeWarning), expectedWarnings); + fs.writeFileSync(`test/css/samples/${dir}/_actual.css`, dom.css); const expected = { html: read(`test/css/samples/${dir}/expected.html`), diff --git a/test/css/samples/omit-scoping-attribute-descendant/_config.js b/test/css/samples/omit-scoping-attribute-descendant/_config.js index b37866f9b6..0d56c7ba8c 100644 --- a/test/css/samples/omit-scoping-attribute-descendant/_config.js +++ b/test/css/samples/omit-scoping-attribute-descendant/_config.js @@ -1,3 +1,19 @@ export default { - cascade: false + cascade: false, + + warnings: [{ + message: 'Unused CSS selector', + loc: { + line: 8, + column: 1 + }, + pos: 74, + frame: ` + 6: + 7: \ No newline at end of file diff --git a/test/css/samples/unused-selector/warnings.json b/test/css/samples/unused-selector/warnings.json new file mode 100644 index 0000000000..6d6c3f5deb --- /dev/null +++ b/test/css/samples/unused-selector/warnings.json @@ -0,0 +1,10 @@ +[{ + "filename": "SvelteComponent.html", + "message": "Unused CSS selector", + "loc": { + "line": 8, + "column": 1 + }, + "pos": 61, + "frame": " 6: }\n 7: \n 8: .bar {\n ^\n 9: color: blue;\n10: }" +}] \ No newline at end of file