feat: warn unused exports

pull/3802/head
Tan Li Hau 5 years ago
parent 33c8cd3329
commit 81c5c480e8

@ -18,6 +18,7 @@ import { Ast, CompileOptions, Var, Warning } from '../interfaces';
import error from '../utils/error'; import error from '../utils/error';
import get_code_frame from '../utils/get_code_frame'; import get_code_frame from '../utils/get_code_frame';
import flatten_reference from './utils/flatten_reference'; import flatten_reference from './utils/flatten_reference';
import is_used_as_reference from './utils/is_used_as_reference';
import is_reference from 'is-reference'; import is_reference from 'is-reference';
import TemplateScope from './nodes/shared/TemplateScope'; import TemplateScope from './nodes/shared/TemplateScope';
import fuzzymatch from '../utils/fuzzymatch'; import fuzzymatch from '../utils/fuzzymatch';
@ -168,12 +169,13 @@ export default class Component {
this.tag = this.name.name; this.tag = this.name.name;
} }
this.walk_module_js(); this.walk_module_js_pre_template();
this.walk_instance_js_pre_template(); this.walk_instance_js_pre_template();
this.fragment = new Fragment(this, ast.html); this.fragment = new Fragment(this, ast.html);
this.name = this.get_unique_name(name); this.name = this.get_unique_name(name);
this.walk_module_js_post_template();
this.walk_instance_js_post_template(); this.walk_instance_js_post_template();
if (!compile_options.customElement) this.stylesheet.reify(); if (!compile_options.customElement) this.stylesheet.reify();
@ -346,6 +348,7 @@ export default class Component {
reassigned: v.reassigned || false, reassigned: v.reassigned || false,
referenced: v.referenced || false, referenced: v.referenced || false,
writable: v.writable || false, writable: v.writable || false,
referenced_from_script: v.referenced_from_script || false,
})), })),
stats: this.stats.render(), stats: this.stats.render(),
}; };
@ -447,63 +450,64 @@ export default class Component {
}); });
} }
extract_imports(content) { extract_imports(node) {
for (let i = 0; i < content.body.length; i += 1) { this.imports.push(node);
const node = content.body[i];
if (node.type === 'ImportDeclaration') {
content.body.splice(i--, 1);
this.imports.push(node);
}
}
} }
extract_exports(content) { extract_exports(node) {
let i = content.body.length; if (node.type === 'ExportDefaultDeclaration') {
while (i--) { this.error(node, {
const node = content.body[i]; code: `default-export`,
message: `A component cannot have a default export`,
});
}
if (node.type === 'ExportDefaultDeclaration') { if (node.type === 'ExportNamedDeclaration') {
if (node.source) {
this.error(node, { this.error(node, {
code: `default-export`, code: `not-implemented`,
message: `A component cannot have a default export`, message: `A component currently cannot have an export ... from`,
}); });
} }
if (node.declaration) {
if (node.type === 'ExportNamedDeclaration') { if (node.declaration.type === 'VariableDeclaration') {
if (node.source) { node.declaration.declarations.forEach(declarator => {
this.error(node, { extract_names(declarator.id).forEach(name => {
code: `not-implemented`, const variable = this.var_lookup.get(name);
message: `A component currently cannot have an export ... from`, variable.export_name = name;
if (variable.writable && !(variable.referenced || variable.referenced_from_script)) {
this.warn(declarator, {
code: `unused-export-let`,
message: `${this.name.name} has unused export property '${name}'. If it is for external reference only, please consider using \`export const '${name}'\``
});
}
});
}); });
} else {
const { name } = node.declaration.id;
const variable = this.var_lookup.get(name);
variable.export_name = name;
} }
if (node.declaration) {
if (node.declaration.type === 'VariableDeclaration') {
node.declaration.declarations.forEach(declarator => {
extract_names(declarator.id).forEach(name => {
const variable = this.var_lookup.get(name);
variable.export_name = name;
});
});
} else {
const { name } = node.declaration.id;
const variable = this.var_lookup.get(name); return node.declaration;
variable.export_name = name; } else {
} node.specifiers.forEach(specifier => {
const variable = this.var_lookup.get(specifier.local.name);
content.body[i] = node.declaration; if (variable) {
} else { variable.export_name = specifier.exported.name;
node.specifiers.forEach(specifier => {
const variable = this.var_lookup.get(specifier.local.name);
if (variable) { if (variable.writable && !(variable.referenced || variable.referenced_from_script)) {
variable.export_name = specifier.exported.name; this.warn(specifier, {
code: `unused-export-let`,
message: `${this.name.name} has unused export property '${specifier.exported.name}'. If it is for external reference only, please consider using \`export const '${specifier.exported.name}'\``
});
} }
}); }
});
content.body.splice(i, 1); return null;
}
} }
} }
} }
@ -522,7 +526,7 @@ export default class Component {
}); });
} }
walk_module_js() { walk_module_js_pre_template() {
const component = this; const component = this;
const script = this.ast.module; const script = this.ast.module;
if (!script) return; if (!script) return;
@ -573,9 +577,6 @@ export default class Component {
}); });
} }
}); });
this.extract_imports(script.content);
this.extract_exports(script.content);
} }
walk_instance_js_pre_template() { walk_instance_js_pre_template() {
@ -657,7 +658,10 @@ export default class Component {
this.add_reference(name.slice(1)); this.add_reference(name.slice(1));
const variable = this.var_lookup.get(name.slice(1)); const variable = this.var_lookup.get(name.slice(1));
if (variable) variable.subscribable = true; if (variable) {
variable.subscribable = true;
variable.referenced_from_script = true;
}
} else { } else {
this.add_var({ this.add_var({
name, name,
@ -667,46 +671,83 @@ export default class Component {
} }
}); });
this.extract_imports(script.content); this.track_references_and_mutations();
this.extract_exports(script.content); }
this.track_mutations();
walk_module_js_post_template() {
const script = this.ast.module;
if (!script) return;
const { body } = script.content;
let i = body.length;
while(--i >= 0) {
const node = body[i];
if (node.type === 'ImportDeclaration') {
this.extract_imports(node);
body.splice(i, 1);
}
if (/^Export/.test(node.type)) {
const replacement = this.extract_exports(node);
if (replacement) {
body[i] = replacement;
} else {
body.splice(i, 1);
}
}
}
} }
walk_instance_js_post_template() { walk_instance_js_post_template() {
const script = this.ast.instance; const script = this.ast.instance;
if (!script) return; if (!script) return;
this.warn_on_undefined_store_value_references(); this.post_template_walk();
this.hoist_instance_declarations(); this.hoist_instance_declarations();
this.extract_reactive_declarations(); this.extract_reactive_declarations();
} }
// TODO merge this with other walks that are independent post_template_walk() {
track_mutations() { const script = this.ast.instance;
if (!script) return;
const component = this; const component = this;
const { content } = script;
const { instance_scope, instance_scope_map: map } = this; const { instance_scope, instance_scope_map: map } = this;
let scope = instance_scope; let scope = instance_scope;
walk(this.ast.instance.content, { const toRemove = [];
enter(node) { const remove = (parent, prop, index) => {
toRemove.unshift([parent, prop, index]);
}
walk(content, {
enter(node, parent, prop, index) {
if (map.has(node)) { if (map.has(node)) {
scope = map.get(node); scope = map.get(node);
} }
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') { if (node.type === 'ImportDeclaration') {
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument; component.extract_imports(node);
const names = extract_names(assignee); // TODO: to use actual remove
remove(parent, prop, index);
const deep = assignee.type === 'MemberExpression'; return this.skip();
}
names.forEach(name => { if (/^Export/.test(node.type)) {
if (scope.find_owner(name) === instance_scope) { const replacement = component.extract_exports(node);
const variable = component.var_lookup.get(name); if (replacement) {
variable[deep ? 'mutated' : 'reassigned'] = true; this.replace(replacement);
} } else {
}); // TODO: to use actual remove
remove(parent, prop, index);
}
return this.skip();
} }
component.warn_on_undefined_store_value_references(node, parent, scope);
}, },
leave(node) { leave(node) {
@ -715,37 +756,53 @@ export default class Component {
} }
}, },
}); });
for(const [parent, prop, index] of toRemove) {
if (parent) {
if (index !== null) {
parent[prop].splice(index, 1);
} else {
delete parent[prop];
}
}
}
} }
warn_on_undefined_store_value_references() { track_references_and_mutations() {
// TODO this pattern happens a lot... can we abstract it const script = this.ast.instance;
// (or better still, do fewer AST walks)? if (!script) return;
const component = this; const component = this;
let { instance_scope: scope, instance_scope_map: map } = this; const { content } = script;
const { instance_scope, instance_scope_map: map } = this;
walk(this.ast.instance.content, { let scope = instance_scope;
walk(content, {
enter(node, parent) { enter(node, parent) {
if (map.has(node)) { if (map.has(node)) {
scope = map.get(node); scope = map.get(node);
} }
if ( if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
node.type === 'LabeledStatement' && const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
node.label.name === '$' && const names = extract_names(assignee);
parent.type !== 'Program'
) { const deep = assignee.type === 'MemberExpression';
component.warn(node as any, {
code: 'non-top-level-reactive-declaration', names.forEach(name => {
message: '$: has no effect outside of the top-level', if (scope.find_owner(name) === instance_scope) {
const variable = component.var_lookup.get(name);
variable[deep ? 'mutated' : 'reassigned'] = true;
}
}); });
} }
if (is_reference(node as Node, parent as Node)) { if (is_used_as_reference(node, parent)) {
const object = get_object(node); const object = get_object(node);
const { name } = object; if (scope.find_owner(object.name) === instance_scope) {
const variable = component.var_lookup.get(object.name);
if (name[0] === '$' && !scope.has(name)) { variable.referenced_from_script = true;
component.warn_if_undefined(name, object, null);
} }
} }
}, },
@ -758,6 +815,28 @@ export default class Component {
}); });
} }
warn_on_undefined_store_value_references(node, parent, scope) {
if (
node.type === 'LabeledStatement' &&
node.label.name === '$' &&
parent.type !== 'Program'
) {
this.warn(node as any, {
code: 'non-top-level-reactive-declaration',
message: '$: has no effect outside of the top-level',
});
}
if (is_reference(node as Node, parent as Node)) {
const object = get_object(node);
const { name } = object;
if (name[0] === '$' && !scope.has(name)) {
this.warn_if_undefined(name, object, null);
}
}
}
invalidate(name, value?) { invalidate(name, value?) {
const variable = this.var_lookup.get(name); const variable = this.var_lookup.get(name);

@ -0,0 +1,33 @@
import { Node } from 'estree';
import is_reference from 'is-reference';
export default function is_used_as_reference(
node: Node,
parent: Node
): boolean {
if (!is_reference(node, parent)) {
return false;
}
if (!parent) {
return true;
}
switch (parent.type) {
// disregard the `foo` in `const foo = bar`
case 'VariableDeclarator':
return node !== parent.id;
// disregard the `foo`, `bar` in `function foo(bar){}`
case 'FunctionDeclaration':
// disregard the `foo` in `import { foo } from 'foo'`
case 'ImportSpecifier':
// disregard the `foo` in `import foo from 'foo'`
case 'ImportDefaultSpecifier':
// disregard the `foo` in `import * as foo from 'foo'`
case 'ImportNamespaceSpecifier':
// disregard the `foo` in `export { foo }`
case 'ExportSpecifier':
return false;
default:
return true;
}
}

@ -148,7 +148,8 @@ export interface Var {
module?: boolean; module?: boolean;
mutated?: boolean; mutated?: boolean;
reassigned?: boolean; reassigned?: boolean;
referenced?: boolean; referenced?: boolean; // referenced from template scope
referenced_from_script?: boolean; // referenced from script
writable?: boolean; writable?: boolean;
// used internally, but not exposed // used internally, but not exposed

@ -0,0 +1,21 @@
<script>
var a = 1;
let b = 1;
const c = 1;
var d = 1;
let e = 1;
const f = 1;
export { d, e, f};
export var g = 1;
export let h = 1;
export const i = 1;
export let j = () => {};
export const k = () => {};
export function l() {};
var m = 1;
let n = 1;
const o = 1;
function foo() {
return m + n + o;
}
</script>

@ -0,0 +1,77 @@
[
{
"code": "unused-export-let",
"end": {
"character": 103,
"column": 12,
"line": 8
},
"message": "Component has unused export property 'd'. If it is for external reference only, please consider using `export const 'd'`",
"pos": 102,
"start": {
"character": 102,
"column": 11,
"line": 8
}
},
{
"code": "unused-export-let",
"end": {
"character": 106,
"column": 15,
"line": 8
},
"message": "Component has unused export property 'e'. If it is for external reference only, please consider using `export const 'e'`",
"pos": 105,
"start": {
"character": 105,
"column": 14,
"line": 8
}
},
{
"code": "unused-export-let",
"end": {
"character": 130,
"column": 18,
"line": 9
},
"message": "Component has unused export property 'g'. If it is for external reference only, please consider using `export const 'g'`",
"pos": 125,
"start": {
"character": 125,
"column": 13,
"line": 9
}
},
{
"code": "unused-export-let",
"end": {
"character": 150,
"column": 18,
"line": 10
},
"message": "Component has unused export property 'h'. If it is for external reference only, please consider using `export const 'h'`",
"pos": 145,
"start": {
"character": 145,
"column": 13,
"line": 10
}
},
{
"code": "unused-export-let",
"end": {
"character": 199,
"column": 25,
"line": 12
},
"message": "Component has unused export property 'j'. If it is for external reference only, please consider using `export const 'j'`",
"pos": 187,
"start": {
"character": 187,
"column": 13,
"line": 12
}
}
]

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false writable: false
} }
]); ]);

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false writable: false
} }
]); ]);

@ -9,6 +9,7 @@ export default {
name: 'hoistable_foo', name: 'hoistable_foo',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false writable: false
}, },
{ {
@ -19,6 +20,7 @@ export default {
name: 'foo', name: 'foo',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
} }
]); ]);

@ -9,6 +9,7 @@ export default {
name: 'hoistable_foo', name: 'hoistable_foo',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false writable: false
}, },
{ {
@ -19,6 +20,7 @@ export default {
name: 'foo', name: 'foo',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
} }
]); ]);

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false writable: false
} }
]); ]);

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
} }
]); ]);

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: false, referenced: false,
referenced_from_script: false,
writable: true writable: true
}, },
{ {
@ -19,6 +20,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
} }
]); ]);

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: true,
writable: true writable: true
}, },
{ {
@ -19,6 +20,7 @@ export default {
mutated: false, mutated: false,
reassigned: true, reassigned: true,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
} }
]); ]);

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: false, referenced: false,
referenced_from_script: false,
writable: false writable: false
}, },
{ {
@ -19,6 +20,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: false, referenced: false,
referenced_from_script: false,
writable: false writable: false
}, },
{ {
@ -29,6 +31,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: false, referenced: false,
referenced_from_script: false,
writable: false writable: false
} }
]); ]);

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: true, reassigned: true,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
}, },
{ {
@ -19,6 +20,7 @@ export default {
mutated: true, mutated: true,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false writable: false
} }
]); ]);

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: true, reassigned: true,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
}, },
{ {
@ -19,6 +20,7 @@ export default {
mutated: true, mutated: true,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false writable: false
} }
]); ]);

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
}, },
{ {
@ -19,6 +20,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
}, },
{ {
@ -29,6 +31,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: false, referenced: false,
referenced_from_script: true,
writable: true writable: true
}, },
{ {
@ -39,6 +42,7 @@ export default {
mutated: false, mutated: false,
reassigned: true, reassigned: true,
referenced: true, referenced: true,
referenced_from_script: true,
writable: true writable: true
} }
]); ]);

@ -0,0 +1,160 @@
export default {
test(assert, vars) {
assert.deepEqual(vars, [
{
name: 'i',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: false,
referenced_from_script: false,
},
{
name: 'j',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: false,
referenced_from_script: false,
},
{
name: 'k',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: false,
referenced_from_script: false,
},
{
name: 'a',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: true,
referenced: false,
writable: true,
referenced_from_script: true,
},
{
name: 'b',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: true,
referenced_from_script: true,
},
{
name: 'c',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: true,
referenced_from_script: true,
},
{
name: 'd',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: true,
referenced_from_script: true,
},
{
name: 'e',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: true,
referenced_from_script: false,
},
{
name: 'f',
export_name: 'f',
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: true,
referenced_from_script: false,
},
{
name: 'g',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: true,
referenced_from_script: true,
},
{
name: 'h',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: true,
referenced: false,
writable: true,
referenced_from_script: true,
},
{
name: 'foo',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: false,
referenced_from_script: false,
},
{
name: 'l',
export_name: null,
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
referenced_from_script: true,
writable: false,
},
{
name: 'bar',
export_name: 'bar',
injected: false,
module: false,
mutated: false,
reassigned: false,
referenced: false,
writable: false,
referenced_from_script: false,
},
]);
},
};

@ -0,0 +1,16 @@
<script>
import i from 'foo';
import * as j from 'foo';
import { k } from 'foo';
let a, b, c, d, e, f, g, h;
function foo() {
a = 1;
console.log(b);
return c + d.e + h++ + l();
}
function l() {}
export { f }
export const bar = g;
</script>

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
}, },
{ {
@ -19,6 +20,7 @@ export default {
mutated: true, mutated: true,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
} }
]); ]);

@ -9,6 +9,7 @@ export default {
mutated: false, mutated: false,
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: true,
writable: true writable: true
}, },
{ {
@ -19,6 +20,7 @@ export default {
mutated: true, mutated: true,
reassigned: false, reassigned: false,
referenced: false, referenced: false,
referenced_from_script: false,
writable: true writable: true
} }
]); ]);

@ -9,6 +9,7 @@ export default {
name: 'Bar', name: 'Bar',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false, writable: false,
}, },
{ {
@ -19,6 +20,7 @@ export default {
name: 'foo', name: 'foo',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true, writable: true,
}, },
{ {
@ -29,6 +31,7 @@ export default {
name: 'baz', name: 'baz',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true, writable: true,
}, },
]); ]);

@ -9,6 +9,7 @@ export default {
name: 'hoistable_foo', name: 'hoistable_foo',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false writable: false
}, },
{ {
@ -19,6 +20,7 @@ export default {
name: 'hoistable_bar', name: 'hoistable_bar',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false writable: false
}, },
{ {
@ -29,6 +31,7 @@ export default {
name: 'hoistable_baz', name: 'hoistable_baz',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: false writable: false
}, },
{ {
@ -39,6 +42,7 @@ export default {
name: 'foo', name: 'foo',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
}, },
{ {
@ -49,6 +53,7 @@ export default {
name: 'bar', name: 'bar',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
}, },
{ {
@ -59,6 +64,7 @@ export default {
name: 'baz', name: 'baz',
reassigned: false, reassigned: false,
referenced: true, referenced: true,
referenced_from_script: false,
writable: true writable: true
} }
]); ]);

Loading…
Cancel
Save