Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .chronus/changes/fix-missing-extern-declaration-2026-5-22.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: fix
packages:
- "@typespec/compiler"
---

Report an error when a function is declared in the `$functions` map in a JS file but has no corresponding `extern fn` declaration in TypeSpec. Previously this would silently have no effect.
40 changes: 40 additions & 0 deletions packages/compiler/src/core/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import {
IntersectionExpressionNode,
IntrinsicScalarName,
JsNamespaceDeclarationNode,
JsSourceFileNode,
LiteralNode,
LiteralType,
LocationContext,
Expand Down Expand Up @@ -4848,6 +4849,7 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
checkSourceFile(file);
}

checkOrphanedFunctionImplementations();
internalDecoratorValidation();
assertNoPendingResolutions();
runPostValidators(postCheckValidators);
Expand Down Expand Up @@ -4880,6 +4882,44 @@ export function createChecker(program: Program, resolver: NameResolver): Checker
}
}

/**
* Check that all function implementations exported via $functions have a corresponding
* `extern fn` declaration in TypeSpec. Reports an error for each orphaned implementation.
*/
function checkOrphanedFunctionImplementations() {
for (const file of program.jsSourceFiles.values()) {
checkSymbolTableForOrphanedFunctions(file.symbol.exports, file);
}
}

function checkSymbolTableForOrphanedFunctions(
exports: SymbolTable | undefined,
sourceFile: JsSourceFileNode,
) {
if (!exports) return;
for (const sym of exports.values()) {
if (sym.flags & SymbolFlags.Function && sym.flags & SymbolFlags.Implementation) {
const merged = getMergedSymbol(sym);
const hasFunctionDeclaration = merged.declarations.some(
(d) => d.kind === SyntaxKind.FunctionDeclarationStatement,
);
if (!hasFunctionDeclaration) {
reportCheckerDiagnostic(
createDiagnostic({
code: "missing-extern-declaration",
format: { name: sym.name },
target: sourceFile,
}),
);
}
}
// Recurse into namespace symbols
if (sym.flags & SymbolFlags.Namespace && sym.exports) {
checkSymbolTableForOrphanedFunctions(sym.exports, sourceFile);
}
}
}

/** Report error with duplicate using in the same scope. */
function checkDuplicateUsings(file: TypeSpecScriptNode) {
const duplicateTrackers = new Map<Sym, DuplicateTracker<Sym, UsingStatementNode>>();
Expand Down
9 changes: 9 additions & 0 deletions packages/compiler/src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,15 @@ const diagnostics = {
default: "Extern declaration must have an implementation in JS file.",
},
},
"missing-extern-declaration": {
severity: "error",
description:
"Report when a function is registered in $functions in a JS file but has no corresponding `extern fn` declaration in TypeSpec.",
url: "https://typespec.io/docs/standard-library/diags/missing-extern-declaration",
messages: {
default: paramMessage`Function implementation "${"name"}" is exported in JS via $functions but has no corresponding 'extern fn' declaration in TypeSpec.`,
},
},
"overload-same-parent": {
severity: "error",
messages: {
Expand Down
Loading
Loading