(r: NativeAstResolved)
| 151 | * or unroutable languages, and warnings for name mismatches / extension collisions. |
| 152 | */ |
| 153 | export function buildNativeRegistry(r: NativeAstResolved): NativeRegistry { |
| 154 | const byExt = new Map<string, NativeRegistryEntry>(); |
| 155 | if (r.mode === false) return { byExt }; |
| 156 | |
| 157 | const candidates = r.authoritative |
| 158 | ? [...r.languages] |
| 159 | : listPluginFiles(r.pluginDirs).map((f) => f.lang); |
| 160 | |
| 161 | for (const requested of candidates) { |
| 162 | const loaded = loadPlugin(requested, r.pluginDirs); |
| 163 | if (!loaded) { |
| 164 | if (r.mode === "strict") recordUnavailableLang(r, requested); |
| 165 | continue; |
| 166 | } |
| 167 | |
| 168 | // `describe().languageId` is authoritative for identity; filename is fallback. |
| 169 | // Coerce defensively — metadata is untrusted plugin output (a non-string id |
| 170 | // is ignored rather than allowed to crash the registry build). |
| 171 | const reported = typeof loaded.metadata?.languageId === "string" ? loaded.metadata.languageId : undefined; |
| 172 | const lang = reported && reported.length > 0 ? reported : requested; |
| 173 | if (reported && reported.length > 0 && reported !== requested) { |
| 174 | addWarning( |
| 175 | r, |
| 176 | `plugin "codesight-${requested}-ast.wasm" self-reports languageId "${reported}" — honoring "${reported}" (rename the file to match for explicit --native-ast=${reported})` |
| 177 | ); |
| 178 | // In explicit mode the user asked for `requested`; a file claiming a |
| 179 | // different id doesn't satisfy that request. |
| 180 | if (r.authoritative && !r.languages.has(lang)) { |
| 181 | if (r.mode === "strict") recordUnavailableLang(r, requested); |
| 182 | continue; |
| 183 | } |
| 184 | } |
| 185 | |
| 186 | // Untrusted: keep only string entries so a malformed array (numbers, null, |
| 187 | // objects) can't throw out of the registry build and crash the scan. |
| 188 | const declared = Array.isArray(loaded.metadata?.extensions) |
| 189 | ? loaded.metadata.extensions.filter((e): e is string => typeof e === "string") |
| 190 | : undefined; |
| 191 | const exts = (declared && declared.length > 0 ? declared : DEFAULT_EXTENSIONS[lang]) ?? []; |
| 192 | if (exts.length === 0) { |
| 193 | if (r.mode === "strict") { |
| 194 | r.diagnostics.push({ |
| 195 | lang, |
| 196 | kind: "routes", |
| 197 | reason: "no extensions declared (describe().extensions is required for non-built-in languages)", |
| 198 | }); |
| 199 | } |
| 200 | continue; |
| 201 | } |
| 202 | |
| 203 | const entry: NativeRegistryEntry = { lang, authoritative: r.authoritative, plugin: adaptPlugin(loaded) }; |
| 204 | for (const raw of exts) { |
| 205 | const ext = (raw.startsWith(".") ? raw : "." + raw).toLowerCase(); |
| 206 | const existing = byExt.get(ext); |
| 207 | if (!existing || existing.lang === entry.lang) { |
| 208 | byExt.set(ext, entry); |
| 209 | continue; |
| 210 | } |
no test coverage detected