Add a tsetse rule that disallows mutating exports
This is illegal for goog.modules emitted by tsickle.
This is not enabled by default.
PiperOrigin-RevId: 284600418
diff --git a/internal/tsetse/checker.ts b/internal/tsetse/checker.ts
index 9eef416..52d06f9 100644
--- a/internal/tsetse/checker.ts
+++ b/internal/tsetse/checker.ts
@@ -96,7 +96,7 @@
const thisChecker = this;
this.currentSourceFile = sourceFile;
this.failures = [];
- ts.forEachChild(sourceFile, run);
+ run(sourceFile);
return this.failures;
function run(node: ts.Node) {
diff --git a/internal/tsetse/error_code.ts b/internal/tsetse/error_code.ts
index 71ca712..87b7fd2 100644
--- a/internal/tsetse/error_code.ts
+++ b/internal/tsetse/error_code.ts
@@ -13,4 +13,5 @@
BAN_PROMISE_AS_CONDITION = 21226,
PROPERTY_RENAMING_SAFE = 21227,
CONFORMANCE_PATTERN = 21228,
+ BAN_MUTABLE_EXPORTS = 21229,
}
diff --git a/internal/tsetse/rules/ban_mutable_exports_rule.ts b/internal/tsetse/rules/ban_mutable_exports_rule.ts
new file mode 100644
index 0000000..5b5716d
--- /dev/null
+++ b/internal/tsetse/rules/ban_mutable_exports_rule.ts
@@ -0,0 +1,71 @@
+/**
+ * @fileoverview Bans 'export' of mutable variables.
+ * It is illegal to mutate them, so you might as well use 'const'.
+ */
+
+import * as ts from 'typescript';
+
+import {Checker} from '../checker';
+import {ErrorCode} from '../error_code';
+import {AbstractRule} from '../rule';
+
+const MUTABLE_EXPORTS_EXCEPTION_FILES = [
+ // Allow in d.ts files, which are modelling external JS that doesn't
+ // follow our rules.
+ '.d.ts',
+];
+
+export class Rule extends AbstractRule {
+ readonly ruleName = 'ban-mutable-exports';
+ readonly code = ErrorCode.BAN_MUTABLE_EXPORTS;
+
+ register(checker: Checker) {
+ // Strategy: take all the exports of the file, then look at whether
+ // they're const or not. This is simpler than the alternative of
+ // trying to match all the various kinds of 'export' syntax, such
+ // as 'export var ...', 'export {...}', 'export default ...'.
+ checker.on(ts.SyntaxKind.SourceFile, checkFile, this.code);
+ }
+}
+
+function checkFile(checker: Checker, file: ts.SourceFile) {
+ if (MUTABLE_EXPORTS_EXCEPTION_FILES.some(
+ (suffix) => file.fileName.endsWith(suffix))) {
+ return;
+ }
+ const sym = checker.typeChecker.getSymbolAtLocation(file);
+ if (!sym) return;
+ const exports = checker.typeChecker.getExportsOfModule(sym);
+ for (const exp of exports) {
+ // In the case of
+ // let x = 3; export {x};
+ // The exported symbol is the latter x, but we must dealias to
+ // the former to judge whether it's const or not.
+ let sym = exp;
+ if (sym.flags & ts.SymbolFlags.Alias) {
+ sym = checker.typeChecker.getAliasedSymbol(exp);
+ }
+ const decl = sym.valueDeclaration;
+ if (!decl) continue; // Skip e.g. type declarations.
+
+ if (decl.getSourceFile() !== file) {
+ // Reexports are best warned about in the original file
+ continue;
+ }
+
+ if (!ts.isVariableDeclaration(decl) && !ts.isBindingElement(decl)) {
+ // Skip e.g. class declarations.
+ continue;
+ }
+
+ const isConst = (ts.getCombinedNodeFlags(decl) & ts.NodeFlags.Const) !== 0;
+ if (!isConst) {
+ // Note: show the failure at the exported symbol's declaration site,
+ // not the dealiased 'sym', so that the error message shows at the
+ // 'export' statement and not the variable declaration.
+ checker.addFailureAtNode(
+ exp.declarations[0],
+ `Exports must be const.`);
+ }
+ }
+}
diff --git a/internal/tsetse/tests/ban_mutable_exports/examples.ts b/internal/tsetse/tests/ban_mutable_exports/examples.ts
new file mode 100644
index 0000000..c687cad
--- /dev/null
+++ b/internal/tsetse/tests/ban_mutable_exports/examples.ts
@@ -0,0 +1,30 @@
+/**
+ * @fileoverview Examples for the mutable exports rule.
+ * We expect every 'bad' to be an error, and every 'ok' to pass.
+ * These are checked as expected diagnostics in the BUILD file.
+ */
+
+export let bad1 = 3;
+export var bad2 = 3;
+export var bad3 = 3, bad4 = 3;
+var bad5 = 3;
+export {bad5};
+let bad6 = 3;
+export {bad6};
+export {bad6 as bad6alias};
+var bad7 = 3;
+export {bad7 as default};
+export let {bad8} = {
+ bad8: 3
+};
+export let bad9: unknown;
+
+let ok1 = 3;
+var ok2 = 3;
+export const ok3 = 3;
+const ok4 = 3;
+const ok5 = 3;
+export {ok5};
+export type ok6 = string;
+export function ok7() {}
+export class ok8 {}