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