Skip to content

Commit d03a5de

Browse files
authored
Merge pull request #3802 from tanhauhau/tanhauhau/warn-unused-exports
feat: warn unused exports
2 parents 67bcc05 + 60b240b commit d03a5de

File tree

23 files changed

+512
-88
lines changed

23 files changed

+512
-88
lines changed

src/compiler/compile/Component.ts

+166-87
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Ast, CompileOptions, Var, Warning } from '../interfaces';
1818
import error from '../utils/error';
1919
import get_code_frame from '../utils/get_code_frame';
2020
import flatten_reference from './utils/flatten_reference';
21+
import is_used_as_reference from './utils/is_used_as_reference';
2122
import is_reference from 'is-reference';
2223
import TemplateScope from './nodes/shared/TemplateScope';
2324
import fuzzymatch from '../utils/fuzzymatch';
@@ -168,12 +169,13 @@ export default class Component {
168169
this.tag = this.name.name;
169170
}
170171

171-
this.walk_module_js();
172+
this.walk_module_js_pre_template();
172173
this.walk_instance_js_pre_template();
173174

174175
this.fragment = new Fragment(this, ast.html);
175176
this.name = this.get_unique_name(name);
176177

178+
this.walk_module_js_post_template();
177179
this.walk_instance_js_post_template();
178180

179181
if (!compile_options.customElement) this.stylesheet.reify();
@@ -346,6 +348,7 @@ export default class Component {
346348
reassigned: v.reassigned || false,
347349
referenced: v.referenced || false,
348350
writable: v.writable || false,
351+
referenced_from_script: v.referenced_from_script || false,
349352
})),
350353
stats: this.stats.render(),
351354
};
@@ -447,63 +450,64 @@ export default class Component {
447450
});
448451
}
449452

450-
extract_imports(content) {
451-
for (let i = 0; i < content.body.length; i += 1) {
452-
const node = content.body[i];
453-
454-
if (node.type === 'ImportDeclaration') {
455-
content.body.splice(i--, 1);
456-
this.imports.push(node);
457-
}
458-
}
453+
extract_imports(node) {
454+
this.imports.push(node);
459455
}
460456

461-
extract_exports(content) {
462-
let i = content.body.length;
463-
while (i--) {
464-
const node = content.body[i];
457+
extract_exports(node) {
458+
if (node.type === 'ExportDefaultDeclaration') {
459+
this.error(node, {
460+
code: `default-export`,
461+
message: `A component cannot have a default export`,
462+
});
463+
}
465464

466-
if (node.type === 'ExportDefaultDeclaration') {
465+
if (node.type === 'ExportNamedDeclaration') {
466+
if (node.source) {
467467
this.error(node, {
468-
code: `default-export`,
469-
message: `A component cannot have a default export`,
468+
code: `not-implemented`,
469+
message: `A component currently cannot have an export ... from`,
470470
});
471471
}
472-
473-
if (node.type === 'ExportNamedDeclaration') {
474-
if (node.source) {
475-
this.error(node, {
476-
code: `not-implemented`,
477-
message: `A component currently cannot have an export ... from`,
472+
if (node.declaration) {
473+
if (node.declaration.type === 'VariableDeclaration') {
474+
node.declaration.declarations.forEach(declarator => {
475+
extract_names(declarator.id).forEach(name => {
476+
const variable = this.var_lookup.get(name);
477+
variable.export_name = name;
478+
if (variable.writable && !(variable.referenced || variable.referenced_from_script)) {
479+
this.warn(declarator, {
480+
code: `unused-export-let`,
481+
message: `${this.name.name} has unused export property '${name}'. If it is for external reference only, please consider using \`export const '${name}'\``
482+
});
483+
}
484+
});
478485
});
486+
} else {
487+
const { name } = node.declaration.id;
488+
489+
const variable = this.var_lookup.get(name);
490+
variable.export_name = name;
479491
}
480-
if (node.declaration) {
481-
if (node.declaration.type === 'VariableDeclaration') {
482-
node.declaration.declarations.forEach(declarator => {
483-
extract_names(declarator.id).forEach(name => {
484-
const variable = this.var_lookup.get(name);
485-
variable.export_name = name;
486-
});
487-
});
488-
} else {
489-
const { name } = node.declaration.id;
490492

491-
const variable = this.var_lookup.get(name);
492-
variable.export_name = name;
493-
}
493+
return node.declaration;
494+
} else {
495+
node.specifiers.forEach(specifier => {
496+
const variable = this.var_lookup.get(specifier.local.name);
494497

495-
content.body[i] = node.declaration;
496-
} else {
497-
node.specifiers.forEach(specifier => {
498-
const variable = this.var_lookup.get(specifier.local.name);
498+
if (variable) {
499+
variable.export_name = specifier.exported.name;
499500

500-
if (variable) {
501-
variable.export_name = specifier.exported.name;
501+
if (variable.writable && !(variable.referenced || variable.referenced_from_script)) {
502+
this.warn(specifier, {
503+
code: `unused-export-let`,
504+
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}'\``
505+
});
502506
}
503-
});
507+
}
508+
});
504509

505-
content.body.splice(i, 1);
506-
}
510+
return null;
507511
}
508512
}
509513
}
@@ -522,7 +526,7 @@ export default class Component {
522526
});
523527
}
524528

525-
walk_module_js() {
529+
walk_module_js_pre_template() {
526530
const component = this;
527531
const script = this.ast.module;
528532
if (!script) return;
@@ -573,9 +577,6 @@ export default class Component {
573577
});
574578
}
575579
});
576-
577-
this.extract_imports(script.content);
578-
this.extract_exports(script.content);
579580
}
580581

581582
walk_instance_js_pre_template() {
@@ -657,7 +658,10 @@ export default class Component {
657658
this.add_reference(name.slice(1));
658659

659660
const variable = this.var_lookup.get(name.slice(1));
660-
if (variable) variable.subscribable = true;
661+
if (variable) {
662+
variable.subscribable = true;
663+
variable.referenced_from_script = true;
664+
}
661665
} else {
662666
this.add_var({
663667
name,
@@ -667,46 +671,83 @@ export default class Component {
667671
}
668672
});
669673

670-
this.extract_imports(script.content);
671-
this.extract_exports(script.content);
672-
this.track_mutations();
674+
this.track_references_and_mutations();
675+
}
676+
677+
walk_module_js_post_template() {
678+
const script = this.ast.module;
679+
if (!script) return;
680+
681+
const { body } = script.content;
682+
let i = body.length;
683+
while (--i >= 0) {
684+
const node = body[i];
685+
if (node.type === 'ImportDeclaration') {
686+
this.extract_imports(node);
687+
body.splice(i, 1);
688+
}
689+
690+
if (/^Export/.test(node.type)) {
691+
const replacement = this.extract_exports(node);
692+
if (replacement) {
693+
body[i] = replacement;
694+
} else {
695+
body.splice(i, 1);
696+
}
697+
}
698+
}
673699
}
674700

675701
walk_instance_js_post_template() {
676702
const script = this.ast.instance;
677703
if (!script) return;
678704

679-
this.warn_on_undefined_store_value_references();
705+
this.post_template_walk();
706+
680707
this.hoist_instance_declarations();
681708
this.extract_reactive_declarations();
682709
}
683710

684-
// TODO merge this with other walks that are independent
685-
track_mutations() {
711+
post_template_walk() {
712+
const script = this.ast.instance;
713+
if (!script) return;
714+
686715
const component = this;
716+
const { content } = script;
687717
const { instance_scope, instance_scope_map: map } = this;
688718

689719
let scope = instance_scope;
690720

691-
walk(this.ast.instance.content, {
692-
enter(node) {
721+
const toRemove = [];
722+
const remove = (parent, prop, index) => {
723+
toRemove.unshift([parent, prop, index]);
724+
};
725+
726+
walk(content, {
727+
enter(node, parent, prop, index) {
693728
if (map.has(node)) {
694729
scope = map.get(node);
695730
}
696731

697-
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
698-
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
699-
const names = extract_names(assignee);
700-
701-
const deep = assignee.type === 'MemberExpression';
732+
if (node.type === 'ImportDeclaration') {
733+
component.extract_imports(node);
734+
// TODO: to use actual remove
735+
remove(parent, prop, index);
736+
return this.skip();
737+
}
702738

703-
names.forEach(name => {
704-
if (scope.find_owner(name) === instance_scope) {
705-
const variable = component.var_lookup.get(name);
706-
variable[deep ? 'mutated' : 'reassigned'] = true;
707-
}
708-
});
739+
if (/^Export/.test(node.type)) {
740+
const replacement = component.extract_exports(node);
741+
if (replacement) {
742+
this.replace(replacement);
743+
} else {
744+
// TODO: to use actual remove
745+
remove(parent, prop, index);
746+
}
747+
return this.skip();
709748
}
749+
750+
component.warn_on_undefined_store_value_references(node, parent, scope);
710751
},
711752

712753
leave(node) {
@@ -715,37 +756,53 @@ export default class Component {
715756
}
716757
},
717758
});
759+
760+
for (const [parent, prop, index] of toRemove) {
761+
if (parent) {
762+
if (index !== null) {
763+
parent[prop].splice(index, 1);
764+
} else {
765+
delete parent[prop];
766+
}
767+
}
768+
}
718769
}
719770

720-
warn_on_undefined_store_value_references() {
721-
// TODO this pattern happens a lot... can we abstract it
722-
// (or better still, do fewer AST walks)?
771+
track_references_and_mutations() {
772+
const script = this.ast.instance;
773+
if (!script) return;
774+
723775
const component = this;
724-
let { instance_scope: scope, instance_scope_map: map } = this;
776+
const { content } = script;
777+
const { instance_scope, instance_scope_map: map } = this;
725778

726-
walk(this.ast.instance.content, {
779+
let scope = instance_scope;
780+
781+
walk(content, {
727782
enter(node, parent) {
728783
if (map.has(node)) {
729784
scope = map.get(node);
730785
}
731786

732-
if (
733-
node.type === 'LabeledStatement' &&
734-
node.label.name === '$' &&
735-
parent.type !== 'Program'
736-
) {
737-
component.warn(node as any, {
738-
code: 'non-top-level-reactive-declaration',
739-
message: '$: has no effect outside of the top-level',
787+
if (node.type === 'AssignmentExpression' || node.type === 'UpdateExpression') {
788+
const assignee = node.type === 'AssignmentExpression' ? node.left : node.argument;
789+
const names = extract_names(assignee);
790+
791+
const deep = assignee.type === 'MemberExpression';
792+
793+
names.forEach(name => {
794+
if (scope.find_owner(name) === instance_scope) {
795+
const variable = component.var_lookup.get(name);
796+
variable[deep ? 'mutated' : 'reassigned'] = true;
797+
}
740798
});
741799
}
742800

743-
if (is_reference(node as Node, parent as Node)) {
801+
if (is_used_as_reference(node, parent)) {
744802
const object = get_object(node);
745-
const { name } = object;
746-
747-
if (name[0] === '$' && !scope.has(name)) {
748-
component.warn_if_undefined(name, object, null);
803+
if (scope.find_owner(object.name) === instance_scope) {
804+
const variable = component.var_lookup.get(object.name);
805+
variable.referenced_from_script = true;
749806
}
750807
}
751808
},
@@ -758,6 +815,28 @@ export default class Component {
758815
});
759816
}
760817

818+
warn_on_undefined_store_value_references(node, parent, scope) {
819+
if (
820+
node.type === 'LabeledStatement' &&
821+
node.label.name === '$' &&
822+
parent.type !== 'Program'
823+
) {
824+
this.warn(node as any, {
825+
code: 'non-top-level-reactive-declaration',
826+
message: '$: has no effect outside of the top-level',
827+
});
828+
}
829+
830+
if (is_reference(node as Node, parent as Node)) {
831+
const object = get_object(node);
832+
const { name } = object;
833+
834+
if (name[0] === '$' && !scope.has(name)) {
835+
this.warn_if_undefined(name, object, null);
836+
}
837+
}
838+
}
839+
761840
invalidate(name, value?) {
762841
const variable = this.var_lookup.get(name);
763842

0 commit comments

Comments
 (0)