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 {}