[pitboss] phase 03: Track A.3 — LangEmitter::entry_kinds_supported + actionable Inconclusive hints

This commit is contained in:
pitboss 2026-05-14 03:22:30 -05:00
parent 8211d4fd47
commit 364d09d6a8
16 changed files with 830 additions and 35 deletions

52
src/dynamic/lang/c.rs Normal file
View file

@ -0,0 +1,52 @@
//! C harness emitter (stub).
//!
//! No harness source is generated yet — `emit` returns
//! [`UnsupportedReason::LangUnsupported`]. The module exists so that
//! [`crate::dynamic::lang::entry_kinds_supported`] can advertise the entry
//! kinds Track B will deliver (Phase 16: `main(argc, argv)`,
//! `LLVMFuzzerTestOneInput`, free functions with `(const char*, size_t)` or
//! `(int, char**)` shapes) and so the verifier can surface
//! `Inconclusive(EntryKindUnsupported { … })` instead of dropping C findings.
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::evidence::UnsupportedReason;
/// Zero-sized [`LangEmitter`] handle for C.
pub struct CEmitter;
/// Entry kinds the C emitter intends to support once Phase 16 lands.
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
impl LangEmitter for CEmitter {
fn emit(&self, _spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
Err(UnsupportedReason::LangUnsupported)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
format!(
"c emitter is a stub; once Phase 16 (Track B Rust + C/C++ vertical) lands it will support {SUPPORTED:?} plus libFuzzer + main(argc, argv) shapes — attempted `EntryKind::{attempted}`"
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!CEmitter.entry_kinds_supported().is_empty());
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = CEmitter.entry_kind_hint(EntryKind::LibraryApi);
assert!(hint.contains("LibraryApi"));
assert!(hint.contains("Phase 16"));
}
}

52
src/dynamic/lang/cpp.rs Normal file
View file

@ -0,0 +1,52 @@
//! C++ harness emitter (stub).
//!
//! No harness source is generated yet — `emit` returns
//! [`UnsupportedReason::LangUnsupported`]. The module exists so that
//! [`crate::dynamic::lang::entry_kinds_supported`] can advertise the entry
//! kinds Track B will deliver (Phase 16: `main(argc, argv)`,
//! `LLVMFuzzerTestOneInput`, free functions with `(const char*, size_t)`)
//! and so the verifier can surface `Inconclusive(EntryKindUnsupported { … })`
//! instead of dropping C++ findings.
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::evidence::UnsupportedReason;
/// Zero-sized [`LangEmitter`] handle for C++.
pub struct CppEmitter;
/// Entry kinds the C++ emitter intends to support once Phase 16 lands.
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
impl LangEmitter for CppEmitter {
fn emit(&self, _spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
Err(UnsupportedReason::LangUnsupported)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
format!(
"cpp emitter is a stub; once Phase 16 (Track B Rust + C/C++ vertical) lands it will support {SUPPORTED:?} plus libFuzzer + main(argc, argv) shapes — attempted `EntryKind::{attempted}`"
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!CppEmitter.entry_kinds_supported().is_empty());
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = CppEmitter.entry_kind_hint(EntryKind::CliSubcommand);
assert!(hint.contains("CliSubcommand"));
assert!(hint.contains("Phase 16"));
}
}

View file

@ -24,10 +24,35 @@
//!
//! Build container: `nyx-build-go:{toolchain_id}` (deferred; §19.1).
use crate::dynamic::lang::HarnessSource;
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
/// Zero-sized [`LangEmitter`] handle for Go. Method bodies delegate to the
/// existing free functions in this module.
pub struct GoEmitter;
/// Entry kinds the Go emitter currently understands. Extended in Phase 15
/// (Track B Go vertical) to include `HttpRoute` (`net/http`, gin) and CLI
/// (`flag.Parse`) shapes.
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
impl LangEmitter for GoEmitter {
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
format!(
"go emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add net/http, gin, flag.Parse shapes in phase 15"
)
}
}
/// Emit a Go harness for `spec`.
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
match &spec.payload_slot {
@ -203,6 +228,19 @@ mod tests {
assert_eq!(err, UnsupportedReason::EntryKindUnsupported);
}
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!GoEmitter.entry_kinds_supported().is_empty());
assert!(GoEmitter.entry_kinds_supported().contains(&EntryKind::Function));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = GoEmitter.entry_kind_hint(EntryKind::HttpRoute);
assert!(hint.contains("HttpRoute"));
assert!(hint.contains("phase 15"));
}
#[test]
fn capitalize_first_handles_lowercase() {
assert_eq!(capitalize_first("handleRequest"), "HandleRequest");

View file

@ -26,10 +26,35 @@
//!
//! Build container: `nyx-build-java:{toolchain_id}` (deferred; §19.1).
use crate::dynamic::lang::HarnessSource;
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
/// Zero-sized [`LangEmitter`] handle for Java. Method bodies delegate to the
/// existing free functions in this module.
pub struct JavaEmitter;
/// Entry kinds the Java emitter currently understands. Extended in Phase 14
/// (Track B Java vertical) to include `HttpRoute` (servlet / Spring /
/// Quarkus) and JUnit static-method shapes.
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
impl LangEmitter for JavaEmitter {
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
format!(
"java emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add servlet / Spring / Quarkus shapes in phase 14"
)
}
}
/// Emit a Java harness for `spec`.
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
match &spec.payload_slot {
@ -182,6 +207,21 @@ mod tests {
assert_eq!(err, UnsupportedReason::EntryKindUnsupported);
}
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!JavaEmitter.entry_kinds_supported().is_empty());
assert!(JavaEmitter
.entry_kinds_supported()
.contains(&EntryKind::Function));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = JavaEmitter.entry_kind_hint(EntryKind::HttpRoute);
assert!(hint.contains("HttpRoute"));
assert!(hint.contains("phase 14"));
}
#[test]
fn harness_has_base64_decoder() {
let spec = make_spec(PayloadSlot::Param(0));

View file

@ -19,10 +19,36 @@
//! Build: no compilation step. Command is `node harness.js`.
//! Build container: `nyx-build-node:{toolchain_id}` (deferred; §19.1).
use crate::dynamic::lang::HarnessSource;
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
/// Zero-sized [`LangEmitter`] handle for JavaScript / TypeScript (one
/// emitter, both langs share the same Node.js dispatch). Method bodies
/// delegate to the existing free functions in this module.
pub struct JavaScriptEmitter;
/// Entry kinds the JS / TS emitter currently understands. Extended in
/// Phase 13 (Track B JS + TS vertical) to include `HttpRoute` (Express /
/// Koa / Next), `CliSubcommand`, etc.
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
impl LangEmitter for JavaScriptEmitter {
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
format!(
"javascript / typescript emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add Express / Koa / Next shapes in phase 13"
)
}
}
/// Emit a Node.js harness for `spec`.
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
match &spec.payload_slot {
@ -230,6 +256,21 @@ mod tests {
assert_eq!(harness.entry_subpath, Some("entry.js".to_owned()));
}
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!JavaScriptEmitter.entry_kinds_supported().is_empty());
assert!(JavaScriptEmitter
.entry_kinds_supported()
.contains(&EntryKind::Function));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = JavaScriptEmitter.entry_kind_hint(EntryKind::HttpRoute);
assert!(hint.contains("HttpRoute"));
assert!(hint.contains("phase 13"));
}
#[test]
fn entry_module_name_is_always_entry_to_match_copy_destination() {
// `copy_entry_file` (via `entry_module_filename`) stages every fixture

View file

@ -1,16 +1,29 @@
//! Per-language harness emitters.
//!
//! Each submodule implements `emit(spec) -> HarnessSource` for one language.
//! The top-level [`emit`] function dispatches on `spec.lang`.
//! Each submodule implements [`LangEmitter`] for one language. The top-level
//! [`emit`] function dispatches on `spec.lang` and validates `spec.entry_kind`
//! against the chosen emitter's [`LangEmitter::entry_kinds_supported`] list
//! before delegating, so unsupported entry kinds short-circuit with a typed
//! `UnsupportedReason::EntryKindUnsupported` rather than producing a
//! never-runnable harness.
//!
//! Two free helpers — [`entry_kinds_supported`] and [`entry_kind_hint`] — wrap
//! the trait dispatch so callers outside the harness build path (notably the
//! verifier, which surfaces an `Inconclusive` verdict with the supported list
//! and hint baked in) can advertise capability without instantiating a spec.
pub mod c;
pub mod cpp;
pub mod go;
pub mod java;
pub mod javascript;
pub mod php;
pub mod python;
pub mod ruby;
pub mod rust;
pub mod typescript;
use crate::dynamic::spec::HarnessSpec;
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::evidence::UnsupportedReason;
use crate::symbol::Lang;
@ -33,15 +46,128 @@ pub struct HarnessSource {
pub entry_subpath: Option<String>,
}
/// Per-language harness emitter contract.
///
/// Implementations are zero-sized unit structs (one per `src/dynamic/lang/*.rs`
/// module). The [`emit`](LangEmitter::emit) method is the legacy
/// per-language entry point retained for the build pipeline; the two
/// capability methods are consulted both at dispatch time (`lang::emit`
/// pre-flight check) and by the verifier when constructing
/// `Inconclusive(EntryKindUnsupported { … })`.
pub trait LangEmitter {
/// Build a harness source bundle for `spec`.
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason>;
/// The set of [`EntryKind`] variants this emitter understands.
///
/// Must be non-empty: every emitter advertises at least one shape it can
/// (or will) drive — even stub modules whose `emit` returns
/// `LangUnsupported`. Empty would be indistinguishable from "language
/// not in the dispatch table" and would defeat the structured
/// advertisement that callers consume.
fn entry_kinds_supported(&self) -> &'static [EntryKind];
/// Human-actionable hint produced when `attempted` is not in
/// [`entry_kinds_supported`](LangEmitter::entry_kinds_supported).
///
/// The string is consumed by
/// [`crate::evidence::InconclusiveReason::EntryKindUnsupported::hint`] and
/// surfaces directly to operators triaging dynamic verification gaps;
/// keep it specific (name the supported kinds, name the phase that will
/// extend support).
fn entry_kind_hint(&self, attempted: EntryKind) -> String;
}
/// Dispatch to the appropriate language emitter.
///
/// Validates `spec.entry_kind` against the chosen emitter's supported list
/// before delegating; an unsupported entry kind short-circuits with
/// [`UnsupportedReason::EntryKindUnsupported`] so the verifier can surface a
/// structured `Inconclusive` verdict with the supported list and hint baked
/// in (instead of producing a never-runnable harness).
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
match spec.lang {
Lang::Python => python::emit(spec),
Lang::Rust => rust::emit(spec),
Lang::JavaScript | Lang::TypeScript => javascript::emit(spec),
Lang::Go => go::emit(spec),
Lang::Java => java::emit(spec),
Lang::Php => php::emit(spec),
_ => Err(UnsupportedReason::LangUnsupported),
let supported = entry_kinds_supported(spec.lang);
if !supported.is_empty() && !supported.contains(&spec.entry_kind) {
return Err(UnsupportedReason::EntryKindUnsupported);
}
dispatch(spec.lang, |e| e.emit(spec))
.unwrap_or(Err(UnsupportedReason::LangUnsupported))
}
/// Public free-fn dispatcher for the supported entry kinds of `lang`.
///
/// Returns an empty slice when `lang` has no registered emitter — callers
/// distinguish that from "emitter exists but advertises none" by treating
/// empty as "language unsupported".
pub fn entry_kinds_supported(lang: Lang) -> &'static [EntryKind] {
dispatch(lang, |e| e.entry_kinds_supported()).unwrap_or(&[])
}
/// Public free-fn dispatcher for an emitter's hint about `attempted`.
///
/// Falls back to a generic message when `lang` has no registered emitter so
/// callers do not need to special-case that path.
pub fn entry_kind_hint(lang: Lang, attempted: EntryKind) -> String {
dispatch(lang, |e| e.entry_kind_hint(attempted)).unwrap_or_else(|| {
format!(
"no harness emitter is registered for {lang:?}; attempted {attempted}"
)
})
}
/// Internal helper: invoke `f` against the emitter registered for `lang`,
/// returning `None` when no emitter is registered for that language.
fn dispatch<R>(lang: Lang, f: impl FnOnce(&dyn LangEmitter) -> R) -> Option<R> {
let emitter: Option<&dyn LangEmitter> = match lang {
Lang::Python => Some(&python::PythonEmitter),
Lang::Rust => Some(&rust::RustEmitter),
Lang::JavaScript => Some(&javascript::JavaScriptEmitter),
Lang::TypeScript => Some(&typescript::TypeScriptEmitter),
Lang::Go => Some(&go::GoEmitter),
Lang::Java => Some(&java::JavaEmitter),
Lang::Php => Some(&php::PhpEmitter),
Lang::Ruby => Some(&ruby::RubyEmitter),
Lang::C => Some(&c::CEmitter),
Lang::Cpp => Some(&cpp::CppEmitter),
};
emitter.map(f)
}
#[cfg(test)]
mod tests {
use super::*;
/// Every registered emitter must advertise at least one entry kind so the
/// verifier never produces an empty `supported` list in
/// `Inconclusive(EntryKindUnsupported { supported, .. })`.
#[test]
fn every_lang_advertises_at_least_one_entry_kind() {
for lang in [
Lang::Python,
Lang::Rust,
Lang::JavaScript,
Lang::TypeScript,
Lang::Go,
Lang::Java,
Lang::Php,
Lang::Ruby,
Lang::C,
Lang::Cpp,
] {
let kinds = entry_kinds_supported(lang);
assert!(
!kinds.is_empty(),
"{lang:?} emitter must advertise at least one EntryKind"
);
}
}
#[test]
fn entry_kind_hint_mentions_attempted() {
let hint = entry_kind_hint(Lang::Python, EntryKind::HttpRoute);
assert!(
hint.contains("HttpRoute"),
"hint must mention the attempted entry kind, got: {hint:?}"
);
}
}

View file

@ -18,10 +18,35 @@
//! Build: no compilation step. Command is `php harness.php`.
//! Build container: `nyx-build-php:{toolchain_id}` (deferred; §19.1).
use crate::dynamic::lang::HarnessSource;
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
/// Zero-sized [`LangEmitter`] handle for PHP. Method bodies delegate to the
/// existing free functions in this module.
pub struct PhpEmitter;
/// Entry kinds the PHP emitter currently understands. Extended in Phase 15
/// (Track B PHP vertical) to include `HttpRoute` (Slim / Laravel / Symfony
/// closures) and `CliSubcommand` (`$argv`).
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
impl LangEmitter for PhpEmitter {
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
format!(
"php emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add Slim / Laravel / Symfony route + CLI shapes in phase 15"
)
}
}
/// Emit a PHP harness for `spec`.
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
match &spec.payload_slot {
@ -193,6 +218,21 @@ mod tests {
assert_eq!(harness.entry_subpath, Some("entry.php".to_owned()));
}
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!PhpEmitter.entry_kinds_supported().is_empty());
assert!(PhpEmitter
.entry_kinds_supported()
.contains(&EntryKind::Function));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = PhpEmitter.entry_kind_hint(EntryKind::HttpRoute);
assert!(hint.contains("HttpRoute"));
assert!(hint.contains("phase 15"));
}
#[test]
fn harness_has_base64_decode() {
let spec = make_spec(PayloadSlot::Param(0));

View file

@ -13,10 +13,35 @@
//! - `PayloadSlot::EnvVar(name)` — set env var before calling.
//! - Other slots produce `UnsupportedReason::EntryKindUnsupported`.
use crate::dynamic::lang::HarnessSource;
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
/// Zero-sized [`LangEmitter`] handle for Python. Registered in the
/// `lang::dispatch` table; method bodies delegate to the existing free
/// functions in this module.
pub struct PythonEmitter;
/// Entry kinds the Python emitter currently understands. Extended in Phase 12
/// (Track B Python vertical) to include `HttpRoute`, `CliSubcommand`, etc.
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
impl LangEmitter for PythonEmitter {
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
format!(
"python emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add framework + CLI shapes in phase 12"
)
}
}
/// Emit a Python harness for `spec`.
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
// Validate payload slot.
@ -237,6 +262,21 @@ mod tests {
assert_eq!(module_name("no_ext"), "no_ext");
}
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!PythonEmitter.entry_kinds_supported().is_empty());
assert!(PythonEmitter
.entry_kinds_supported()
.contains(&EntryKind::Function));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = PythonEmitter.entry_kind_hint(EntryKind::HttpRoute);
assert!(hint.contains("HttpRoute"));
assert!(hint.contains("phase 12"));
}
#[test]
fn unsupported_lang_returns_err() {
let mut spec = make_spec(PayloadSlot::Param(0));

77
src/dynamic/lang/ruby.rs Normal file
View file

@ -0,0 +1,77 @@
//! Ruby harness emitter (stub).
//!
//! No harness source is generated yet — `emit` returns
//! [`UnsupportedReason::LangUnsupported`]. The module exists so that
//! [`crate::dynamic::lang::entry_kinds_supported`] can advertise the entry
//! kinds Track B will deliver (Phase 15: Sinatra route, Rails action, Rack
//! middleware, generic controller method) and so the verifier can surface
//! a structured `Inconclusive(EntryKindUnsupported { … })` instead of
//! silently dropping Ruby findings.
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::evidence::UnsupportedReason;
/// Zero-sized [`LangEmitter`] handle for Ruby.
pub struct RubyEmitter;
/// Entry kinds the Ruby emitter intends to support once Phase 15 lands.
/// Advertised pre-implementation so the verifier can route findings into
/// `Inconclusive(EntryKindUnsupported)` rather than `Unsupported`.
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
impl LangEmitter for RubyEmitter {
fn emit(&self, _spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
Err(UnsupportedReason::LangUnsupported)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
format!(
"ruby emitter is a stub; once Phase 15 (Track B Ruby vertical) lands it will support {SUPPORTED:?} plus Sinatra / Rails / Rack route shapes — attempted `EntryKind::{attempted}`"
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!RubyEmitter.entry_kinds_supported().is_empty());
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = RubyEmitter.entry_kind_hint(EntryKind::HttpRoute);
assert!(hint.contains("HttpRoute"));
assert!(hint.contains("Phase 15"));
}
#[test]
fn emit_returns_lang_unsupported() {
let spec = HarnessSpec {
finding_id: "0".into(),
entry_file: "x.rb".into(),
entry_name: "f".into(),
entry_kind: EntryKind::Function,
lang: crate::symbol::Lang::Ruby,
toolchain_id: "ruby-3".into(),
payload_slot: crate::dynamic::spec::PayloadSlot::Param(0),
expected_cap: crate::labels::Cap::SQL_QUERY,
constraint_hints: vec![],
sink_file: "x.rb".into(),
sink_line: 1,
spec_hash: "0".into(),
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
};
assert_eq!(
RubyEmitter.emit(&spec).unwrap_err(),
UnsupportedReason::LangUnsupported
);
}
}

View file

@ -21,11 +21,36 @@
//!
//! HTML_ESCAPE is n/a for Rust (§15.4).
use crate::dynamic::lang::HarnessSource;
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::labels::Cap;
/// Zero-sized [`LangEmitter`] handle for Rust. Method bodies delegate to the
/// existing free functions in this module.
pub struct RustEmitter;
/// Entry kinds the Rust emitter currently understands. Extended in Phase 16
/// (Track B Rust + C/C++ vertical) to include `HttpRoute` (`actix_web`,
/// `axum`), `CliSubcommand` (clap), and `LibraryApi` (libfuzzer).
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
impl LangEmitter for RustEmitter {
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
format!(
"rust emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add actix / axum / clap / libfuzzer shapes in phase 16"
)
}
}
/// Emit a Rust harness for `spec`.
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
match &spec.payload_slot {
@ -247,6 +272,21 @@ mod tests {
assert!(cargo.contains("path = \"src/main.rs\""));
}
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!RustEmitter.entry_kinds_supported().is_empty());
assert!(RustEmitter
.entry_kinds_supported()
.contains(&EntryKind::Function));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = RustEmitter.entry_kind_hint(EntryKind::HttpRoute);
assert!(hint.contains("HttpRoute"));
assert!(hint.contains("phase 16"));
}
#[test]
fn b64_decode_roundtrip() {
// Test by compiling: actual b64_decode is in generated code.

View file

@ -0,0 +1,64 @@
//! TypeScript harness emitter.
//!
//! Today TypeScript shares the JS emitter — `tsc` is not invoked; the runner
//! treats `.ts` / `.tsx` / `.mts` / `.cts` files as Node-compatible because
//! every shape we currently emit (free functions, `module.exports`-style
//! handlers) is identical at the runtime level after type erasure. This
//! module exists so the [`crate::dynamic::lang::LangEmitter`] dispatch table
//! has a discoverable per-language handle and so callers can call
//! `entry_kinds_supported(Lang::TypeScript)` symmetrically with the other
//! languages — the actual `emit` body delegates to
//! [`crate::dynamic::lang::javascript::emit`].
//!
//! Phase 13 (Track B JS + TS vertical) introduces TS-specific shapes
//! (Next.js route handlers, `tsx` browser modules under jsdom). When those
//! land, the supported list / hint shift here without affecting the JS
//! emitter.
use crate::dynamic::lang::{javascript, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::evidence::UnsupportedReason;
/// Zero-sized [`LangEmitter`] handle for TypeScript.
pub struct TypeScriptEmitter;
/// Entry kinds the TypeScript emitter currently understands. Same as JS until
/// Phase 13 introduces TS-specific shapes (Next.js route handlers, `tsx`
/// browser modules).
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
impl LangEmitter for TypeScriptEmitter {
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
javascript::emit(spec)
}
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
SUPPORTED
}
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
format!(
"typescript emitter supports {SUPPORTED:?} (delegates to the JavaScript emitter); this finding's enclosing context is `EntryKind::{attempted}` — Track B will add Next.js / jsdom shapes in phase 13"
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn entry_kinds_supported_is_non_empty() {
assert!(!TypeScriptEmitter.entry_kinds_supported().is_empty());
assert!(TypeScriptEmitter
.entry_kinds_supported()
.contains(&EntryKind::Function));
}
#[test]
fn entry_kind_hint_names_attempted_and_phase() {
let hint = TypeScriptEmitter.entry_kind_hint(EntryKind::HttpRoute);
assert!(hint.contains("HttpRoute"));
assert!(hint.contains("phase 13"));
}
}