Files
fs2open.github.com/code/scripting/doc_html.cpp
T
Goober5000andClaude Opus 4.8 bed228d264 avoid undefined behavior from UNREACHABLE on reachable code paths (#7555)
* avoid undefined behavior from UNREACHABLE on reachable code paths

UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.

Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.

A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.

Where the converted site was a simple guard of the form
    if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
    Assertion(!x, ...);
    if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* addendum

After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible.  UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error".  Also fix a few other sites and change a couple to Warnings.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-09 11:20:25 +09:00

640 lines
17 KiB
C++

#include "doc_html.h"
#include "scripting/api/objs/enums.h"
#include <cctype>
namespace scripting {
namespace {
SCP_string link_enum_group_references(const SCP_string& text) {
SCP_string out;
SCP_string token;
for (size_t i = 0; i <= text.size(); ++i) {
const auto ch = (i < text.size()) ? text[i] : ' ';
if (std::isalnum(static_cast<unsigned char>(ch)) || ch == '_' || ch == '*') {
token.push_back(ch);
continue;
}
if (!token.empty()) {
SCP_string lookup = token;
while (!lookup.empty() && (lookup.back() == '*' || lookup.back() == ',' || lookup.back() == '.' || lookup.back() == ')' || lookup.back() == ']')) {
lookup.pop_back();
}
auto group = api::get_enum_group_info(lookup.c_str());
if (group) {
out += "<a href=\"#enum-group-";
out += group->id;
out += "\">";
out += token;
out += "</a>";
} else {
out += token;
}
token.clear();
}
if (i < text.size()) {
out.push_back(ch);
}
}
return out;
}
void ade_output_toc(FILE* fp, const std::unique_ptr<DocumentationElement>& el)
{
Assert(fp != nullptr);
Assert(el != nullptr);
// WMC - sanity checking
if (el->name.empty() && el->shortName.empty()) {
Warning(LOCATION, "Found ade_table_entry with no name or shortname");
return;
}
fputs("<dd>", fp);
if (el->name.empty()) {
fprintf(fp, "<a href=\"#%s\">%s", el->shortName.c_str(), el->shortName.c_str());
} else {
fprintf(fp, "<a href=\"#%s\">%s", el->name.c_str(), el->name.c_str());
if (!el->shortName.empty())
fprintf(fp, " (%s)", el->shortName.c_str());
}
fputs("</a>", fp);
if (!el->description.empty()) {
auto description = link_enum_group_references(el->description);
fprintf(fp, " - %s\n", description.c_str());
}
fputs("</dd>\n", fp);
}
void OutputToc(FILE* fp, const ScriptingDocumentation& doc)
{
//***TOC: Libraries
fputs("<dt><b>Libraries</b></dt>\n", fp);
for (const auto& el : doc.elements) {
if (el->type != ElementType::Library) {
continue;
}
ade_output_toc(fp, el);
}
//***TOC: Objects
fputs("<dt><b>Types</b></dt>\n", fp);
for (const auto& el : doc.elements) {
if (el->type != ElementType::Class) {
continue;
}
ade_output_toc(fp, el);
}
//***TOC: Enumerations
fputs("<dt><b><a href=\"#Enumerations\">Enumerations</a></b></dt>", fp);
//***End TOC
fputs("</dl><br/><br/>", fp);
}
SCP_string element_name(const std::unique_ptr<DocumentationElement>& el)
{
if (!el->name.empty()) {
return el->name;
}
return el->shortName;
}
void ade_output_type_link(FILE* fp, const ade_type_info& type_info)
{
switch (type_info.getType()) {
case ade_type_info_type::Empty:
fputs("nothing", fp);
break;
case ade_type_info_type::Simple:
if (ade_is_internal_type(type_info.getIdentifier())) {
fputs(type_info.getIdentifier(), fp);
} else {
fprintf(fp, "<a href=\"#%s\">%s</a>", type_info.getIdentifier(), type_info.getIdentifier());
}
break;
case ade_type_info_type::Tuple: {
bool first = true;
for (const auto& type : type_info.elements()) {
if (!first) {
fprintf(fp, ", ");
}
first = false;
ade_output_type_link(fp, type);
}
break;
}
case ade_type_info_type::Array:
fputs("{ ", fp);
ade_output_type_link(fp, type_info.arrayType());
fputs(" ... }", fp);
break;
case ade_type_info_type::Map:
fputs("{ ", fp);
ade_output_type_link(fp, type_info.elements()[0]);
fputs(" -> ", fp);
ade_output_type_link(fp, type_info.elements()[1]);
fputs(" ... }", fp);
break;
case ade_type_info_type::Iterator:
fputs("iterator< ", fp);
ade_output_type_link(fp, type_info.arrayType());
fputs(" >", fp);
break;
case ade_type_info_type::Alternative: {
bool first = true;
for (const auto& type : type_info.elements()) {
if (!first) {
fputs(" | ", fp);
}
first = false;
ade_output_type_link(fp, type);
}
break;
}
case ade_type_info_type::Function: {
const auto& elements = type_info.elements();
if (elements.size() == 1) {
fputs("function() -> ", fp);
} else {
fputs("function(", fp);
bool first = true;
for (auto iter = elements.begin() + 1; iter != elements.end(); ++iter) {
if (!first) {
fputs(", ", fp);
}
first = false;
ade_output_type_link(fp, *iter);
fprintf(fp, " %s", iter->getName().c_str());
}
fputs(") -> ", fp);
}
ade_output_type_link(fp, elements.front());
break;
}
case ade_type_info_type::Generic: {
const auto& elements = type_info.elements();
ade_output_type_link(fp, elements.front());
if (elements.size() > 1) {
fputs("<", fp);
bool first = true;
for (auto iter = elements.begin() + 1; iter != elements.end(); ++iter) {
if (!first) {
fputs(", ", fp);
}
first = false;
ade_output_type_link(fp, *iter);
}
fputs(">", fp);
}
break;
}
case ade_type_info_type::Varargs: {
ade_output_type_link(fp, type_info.elements().front());
fputs("...", fp);
break;
}
default:
UNREACHABLE("Unhandled type %d!", static_cast<int>(type_info.getType()));
break;
}
}
void output_argument_list(FILE* fp, const DocumentationElementFunction::argument_list& overload)
{
if (overload.simple.empty()) {
bool first = true;
bool optional = false;
for (const auto& arg : overload.arguments) {
if (!first) {
fputs(", ", fp);
}
first = false;
if (arg.optional && !optional) {
fputs("[", fp);
optional = true;
}
ade_output_type_link(fp, arg.type);
if (!arg.name.empty()) {
fprintf(fp, " <i>%s</i>", arg.name.c_str());
if (!arg.def_val.empty()) {
fprintf(fp, " = <i>%s</i>", link_enum_group_references(arg.def_val).c_str());
}
}
if (!arg.comment.empty()) {
fprintf(fp, " (%s)", link_enum_group_references(arg.comment).c_str());
}
}
if (optional) {
fputs("]", fp);
}
} else {
fprintf(fp, "<i>%s</i>", link_enum_group_references(overload.simple).c_str());
}
}
void OutputElement(FILE* fp,
const std::unique_ptr<DocumentationElement>& el,
const DocumentationElement* parent,
const DocumentationElement* parentParent)
{
if (el->name.empty() && el->shortName.empty()) {
Warning(LOCATION, "Found ade_table_entry with no name or shortname");
return;
}
bool skip_this = false;
// WMC - Hack
if (parent != nullptr) {
for (const auto& child : el->children) {
if (child->name == "__indexer") {
OutputElement(fp, child, el.get(), parent);
skip_this = true;
break;
}
}
}
//***Begin entry
if (!skip_this) {
fputs("<dd><dl>", fp);
switch (el->type) {
case ElementType::Library:
case ElementType::Class: {
//***Name (ShortName)
if (parent == nullptr) {
fprintf(fp, "<dt id=\"%s\">", element_name(el).c_str());
}
if (el->name.empty()) {
if (el->type == ElementType::Class) {
const auto classEl = static_cast<DocumentationElementClass*>(el.get());
if (classEl->superClass.empty())
fprintf(fp, "<h2>%s</h2>\n", classEl->shortName.c_str());
else {
fprintf(fp,
"<h2>%s:<a href=\"#%s\">%s</a></h2>\n",
classEl->superClass.c_str(),
classEl->superClass.c_str(),
classEl->superClass.c_str());
}
} else {
fprintf(fp, "<h2>%s</h2>\n", el->shortName.c_str());
}
} else {
fprintf(fp, "<h2>%s", el->name.c_str());
if (!el->shortName.empty())
fprintf(fp, " (%s)", el->shortName.c_str());
if (el->type == ElementType::Class) {
const auto classEl = static_cast<DocumentationElementClass*>(el.get());
if (!classEl->superClass.empty()) {
fprintf(fp,
":<a href=\"#%s\">%s</a>",
classEl->superClass.c_str(),
classEl->superClass.c_str());
}
}
fputs("</h2>\n", fp);
}
fputs("</dt>\n", fp);
//***Description
if (!el->description.empty()) {
auto description = link_enum_group_references(el->description);
fprintf(fp, "<dd>%s</dd>\n", description.c_str());
}
break;
}
case ElementType::Operator:
case ElementType::Function: {
const auto funcEl = static_cast<DocumentationElementFunction*>(el.get());
//***Name(ShortName)(Arguments)
for (const auto& overload : funcEl->overloads) {
fputs("<dt>", fp);
fputs("<i>", fp);
if (!funcEl->returnType.isEmpty())
ade_output_type_link(fp, funcEl->returnType);
else
fputs("nil", fp);
fputs(" </i>", fp);
const string_conv* op = nullptr;
if (el->name.empty()) {
fprintf(fp, "<b>%s", el->shortName.c_str());
} else {
op = ade_get_operator(el->name.c_str());
// WMC - Do we have an operator?
if (op == nullptr) {
fprintf(fp, "<b>%s", el->name.c_str());
if (!el->shortName.empty()) {
const auto op2 = ade_get_operator(el->shortName.c_str());
if (op2 == nullptr)
fprintf(fp, "(%s)", el->shortName.c_str());
else
fprintf(fp, "(%s)", op2->dest);
}
} else {
// WMC - Hack
if (parent != nullptr && !parent->name.empty() && parentParent != nullptr &&
el->name == "__indexer") {
fprintf(fp, "<b>%s%s", parent->name.c_str(), op->dest);
} else
fprintf(fp, "<b>%s", op->dest);
}
}
if (op == nullptr) {
fputs("(</b>", fp);
output_argument_list(fp, overload);
fputs("<b>)</b>\n", fp);
} else {
fputs("</b> ", fp);
output_argument_list(fp, overload);
}
fputs("</dt>\n", fp);
}
//***Description
if (!el->description.empty()) {
auto description = link_enum_group_references(el->description);
fprintf(fp, "<dd>%s</dd>\n", description.c_str());
}
if (el->deprecationVersion.isValid()) {
if (!el->deprecationMessage.empty()) {
fprintf(fp,
"<dd><b>Deprecated starting with version %d.%d.%d:</b> %s</dd>\n",
el->deprecationVersion.major,
el->deprecationVersion.minor,
el->deprecationVersion.build,
el->deprecationMessage.c_str());
} else {
fprintf(fp,
"<dd><b>Deprecated starting with version %d.%d.%d.</b></dd>\n",
el->deprecationVersion.major,
el->deprecationVersion.minor,
el->deprecationVersion.build);
}
}
//***Result: ReturnDescription
if (!funcEl->returnDocumentation.empty()) {
auto returns = link_enum_group_references(funcEl->returnDocumentation);
fprintf(fp, "<dd><b>Returns:</b> %s</dd>\n", returns.c_str());
} else {
fputs("<dd><b>Returns:</b> Nothing</dd>\n", fp);
}
break;
}
case ElementType::Property: {
const auto propEl = static_cast<DocumentationElementProperty*>(el.get());
//***Type Name(ShortName)
fputs("<dt>\n", fp);
fputs("<i>", fp);
ade_output_type_link(fp, propEl->getterType);
fputs("</i> ", fp);
if (propEl->name.empty()) {
fprintf(fp, "<b>%s</b>\n", propEl->shortName.c_str());
} else {
fprintf(fp, "<b>%s", propEl->name.c_str());
if (!propEl->shortName.empty())
fputs(propEl->shortName.c_str(), fp);
fputs("</b>\n", fp);
}
if (!propEl->setterType.isEmpty()) {
fprintf(fp, " = ");
ade_output_type_link(fp, propEl->setterType);
}
fputs("</dt>\n", fp);
//***Description
if (!propEl->description.empty()) {
auto description = link_enum_group_references(propEl->description);
fprintf(fp, "<dd>%s</dd>\n", description.c_str());
}
if (!propEl->returnDocumentation.empty())
{
auto value = link_enum_group_references(propEl->returnDocumentation);
fprintf(fp, "<dd><b>Value:</b> %s</b></dd>\n", value.c_str());
}
break;
}
default:
Warning(LOCATION, "Unknown type '%d' passed to ade_table_entry::OutputMeta", static_cast<int>(el->type));
break;
}
}
fputs("<dd><dl>\n", fp);
for (const auto& child : el->children) {
if (parent == nullptr || child->name != "__indexer")
OutputElement(fp, child, el.get(), parent);
}
fputs("</dl></dd>\n", fp);
if (!skip_this)
fputs("<br></dl></dd>\n", fp);
}
void OutputLuaMeta(FILE* fp, const ScriptingDocumentation& doc)
{
OutputToc(fp, doc);
SCP_vector<ade_table_entry*> table_entries;
//***Everything
fputs("<dl>\n", fp);
for (const auto& el : doc.elements) {
OutputElement(fp, el, nullptr, nullptr);
}
fputs("</dl>\n", fp);
}
template <typename Container>
static void output_hook_variable_list(FILE* fp, const Container& vars)
{
fputs("<dl>", fp);
for (const auto& param : vars) {
fputs("<dt>", fp);
ade_output_type_link(fp, param.type);
fprintf(fp, " <i>%s</i></dt>", param.name);
fprintf(fp, "<dd><b>Description:</b> %s</dd>", link_enum_group_references(param.description).c_str());
}
fputs("</dl>", fp);
}
template <typename Container>
static void output_hook_conditions_list(FILE* fp, const Container& vars)
{
fputs("<dl>", fp);
for (const auto& param : vars) {
fprintf(fp, "<dt><b>%s</b></dt><dd><b>Description:</b> %s</dd>", param.first.c_str(), param.second->documentation.c_str());
}
fputs("</dl>", fp);
}
} // namespace
void output_html_doc(const ScriptingDocumentation& doc, const SCP_string& filename)
{
FILE* fp = fopen(filename.c_str(), "w");
if (fp == nullptr) {
return;
}
fprintf(fp,
"<html>\n<head>\n\t<title>Script Output - FSO v%s (%s)</title>\n</head>\n",
FS_VERSION_FULL,
doc.name.c_str());
fputs("<body>", fp);
fprintf(fp, "\t<h1>Script Output - FSO v%s (%s)</h1>\n", FS_VERSION_FULL, doc.name.c_str());
// Scripting languages links
fputs("<dl>", fp);
//***Hooks
fputs("<dt><h2>Conditional Hooks</h2></dt>", fp);
fputs("<dd><dl>", fp);
// Conditions
fputs("<dt><b>Conditions</b></dt>", fp);
for (const auto& conditions : doc.conditions) {
fprintf(fp, "<dd>%s</dd>", conditions.c_str());
}
// Actions
fputs("<dt><b>Actions</b></dt>", fp);
fputs("<dd><dl>", fp);
for (const auto& hook : doc.actions) {
fprintf(fp, "<dt><b>%s</b></dt>", hook.name.c_str());
if (!hook.description.empty()) {
fprintf(fp, "<dd><b>Description:</b> %s", hook.description.c_str());
// Only write this for hooks with descriptions since this information is useless for legacy hooks
if (hook.overridable) {
fputs("<br><i>This hook is overridable</i>", fp);
} else {
fputs("<br><i>This hook is <b>not</b> overridable</i>", fp);
}
if (hook.deprecation) {
if (hook.deprecation->level_hook == HookDeprecationOptions::DeprecationLevel::LEVEL_ERROR) {
fprintf(fp,
"<br><b>Removed starting with version %d.%d.%d.</b>\n",
hook.deprecation->deprecatedSince.major,
hook.deprecation->deprecatedSince.minor,
hook.deprecation->deprecatedSince.build);
}
else if (hook.deprecation->level_override == HookDeprecationOptions::DeprecationLevel::LEVEL_ERROR) {
fprintf(fp,
"<br><b>Deprecated (+Override removed) starting with version %d.%d.%d.</b>\n",
hook.deprecation->deprecatedSince.major,
hook.deprecation->deprecatedSince.minor,
hook.deprecation->deprecatedSince.build);
}
else {
fprintf(fp,
"<br><b>Deprecated starting with version %d.%d.%d.</b>\n",
hook.deprecation->deprecatedSince.major,
hook.deprecation->deprecatedSince.minor,
hook.deprecation->deprecatedSince.build);
}
}
if (!hook.parameters.empty()) {
fputs("<br><b>Hook Variables:</b>", fp);
output_hook_variable_list(fp, hook.parameters);
}
if (!hook.conditions.empty()) {
fputs("<b>Hook-specific Conditions:</b>", fp);
output_hook_conditions_list(fp, hook.conditions);
}
fputs("</dd>", fp);
}
}
fputs("</dl></dd>", fp);
fputs("<dt><b>Global Hook Variables:</b> (set globally independent of a specific hook)</dt>", fp);
output_hook_variable_list(fp, doc.globalVariables);
fputs("</dl></dd><br />", fp);
// Languages
fputs("<dl>", fp);
//***Version info
fprintf(fp, "<dd>Lua Version: %s</dd>\n", LUA_RELEASE);
fputs("<dd>", fp);
OutputLuaMeta(fp, doc);
fputs("</dd>", fp);
//***Options
fputs("<dt><b>In Game Options</b></dt>", fp);
fputs("<dd><dl>", fp);
for (const auto& option : doc.options) {
fprintf(fp, "<dt><b>%s</b></dt>", option.title.c_str());
fprintf(fp, "<dd><b>Key:</b> %s</dd>", option.key.c_str());
fprintf(fp, "<dd><b>Description:</b> %s</dd>", option.description.c_str());
}
//***Enumerations
fprintf(fp, "<dt id=\"Enumerations\"><h2>Enumerations</h2></dt>");
SCP_unordered_map<SCP_string, SCP_vector<const DocumentationEnum*>> enum_groups;
SCP_vector<SCP_string> enum_group_order;
for (const auto& enumeration : doc.enumerations) {
auto key = enumeration.group_id.empty() ? SCP_string("ungrouped") : enumeration.group_id;
if (enum_groups.find(key) == enum_groups.end()) {
enum_group_order.push_back(key);
}
enum_groups[key].push_back(&enumeration);
}
for (const auto& group_id : enum_group_order) {
const auto* first_enum = enum_groups[group_id].front();
const auto& group_title = first_enum->group_title.empty() ? SCP_string("Ungrouped Enumerations") : first_enum->group_title;
const auto& group_description = first_enum->group_description.empty() ? SCP_string("No group description found. Add a matching prefix entry to get_enum_group_info() in enums.cpp.") : first_enum->group_description;
fprintf(fp, "<dt><h3 id=\"enum-group-%s\">%s</h3></dt>", group_id.c_str(), group_title.c_str());
fprintf(fp, "<dd>%s</dd>", group_description.c_str());
fputs("<dd><dl>", fp);
for (const auto* enumeration : enum_groups[group_id]) {
if (!enumeration->description.empty()) {
fprintf(fp, "<dd><span style=\"font-size: 0.92em;\"><b>%s</b> - %s</span></dd>",
enumeration->name.c_str(), enumeration->description.c_str());
} else {
fprintf(fp, "<dd><span style=\"font-size: 0.92em;\"><b>%s</b></span></dd>", enumeration->name.c_str());
}
}
fputs("</dl></dd>", fp);
}
fputs("</dl></body></html>", fp);
fclose(fp);
}
} // namespace scripting