1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::compiler_error::CompileError;
8use crate::{Asi, Diagnostic, FileId, Severity, Span, Token, TokenKind};
9
10#[derive(Clone, Debug, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum ConfigValue {
13 String(#[serde(with = "string_value")] String),
15 Number(f64),
16 Boolean(bool),
17}
18
19impl PartialEq for ConfigValue {
20 fn eq(&self, other: &Self) -> bool {
21 match (self, other) {
22 (Self::String(left), Self::String(right)) => left == right,
23 (Self::Number(left), Self::Number(right)) => left.to_bits() == right.to_bits(),
24 (Self::Boolean(left), Self::Boolean(right)) => left == right,
25 _ => false,
26 }
27 }
28}
29
30impl Eq for ConfigValue {}
33
34impl ConfigValue {
35 pub fn type_name(&self) -> &'static str {
36 match self {
37 Self::String(_) => "string",
38 Self::Number(_) => "number",
39 Self::Boolean(_) => "boolean",
40 }
41 }
42
43 fn token(&self) -> TokenKind {
44 match self {
45 Self::String(value) => TokenKind::StringLiteral(value.clone()),
46 Self::Number(value) => TokenKind::NumberLiteral(*value),
47 Self::Boolean(value) => TokenKind::BooleanLiteral(*value),
48 }
49 }
50}
51
52#[derive(Clone, Debug)]
53pub struct ConfigParameter {
54 pub name: String,
55 pub type_name: &'static str,
56 pub default: Option<ConfigValue>,
57 pub span: Span,
58 header: Vec<Token>,
59 value_span: Span,
60}
61
62pub fn parameters(source: &str, file: FileId) -> Result<Vec<ConfigParameter>, CompileError> {
63 let mut asi = Asi::new(source, file);
64 let mut tokens = Vec::new();
65 loop {
66 let token = asi.next_token();
67 let eof = token.kind == TokenKind::Eof;
68 tokens.push(token);
69 if eof {
70 break;
71 }
72 }
73 let diagnostics = asi.finish()?;
74 if diagnostics
75 .iter()
76 .any(|diag| diag.severity == Severity::Error)
77 {
78 return Err(diagnostics.into());
79 }
80 let mut reader = ConfigReader {
81 source,
82 tokens: &tokens,
83 pos: 0,
84 };
85 let mut result = Vec::new();
86 let mut names = std::collections::BTreeSet::new();
87 while reader.peek().kind != TokenKind::Eof {
88 if reader.take(&TokenKind::Semicolon) {
89 continue;
90 }
91 let parameter = reader.parameter()?;
92 if !names.insert(parameter.name.clone()) {
93 return Err(error(
94 parameter.span,
95 format!("duplicate configuration parameter `{}`", parameter.name),
96 ));
97 }
98 result.push(parameter);
99 }
100 Ok(result)
101}
102
103pub fn validate_inputs(
105 parameters: &[ConfigParameter],
106 inputs: &BTreeMap<String, String>,
107 file: FileId,
108) -> Result<(), CompileError> {
109 for name in inputs.keys() {
110 if !parameters.iter().any(|parameter| ¶meter.name == name) {
111 return Err(error(
112 Span::at(file),
113 format!(
114 "unknown configuration parameter `{name}`; declare it in src/config.ts or remove the override with --reset-config {name}"
115 ),
116 ));
117 }
118 }
119 for parameter in parameters {
120 if let Some(input) = inputs.get(¶meter.name) {
121 parse_input(parameter, input)?;
122 }
123 }
124 Ok(())
125}
126
127pub fn resolve(
128 parameters: &[ConfigParameter],
129 inputs: &BTreeMap<String, String>,
130 file: FileId,
131) -> Result<BTreeMap<String, ConfigValue>, CompileError> {
132 validate_inputs(parameters, inputs, file)?;
133 let mut values = BTreeMap::new();
134 let mut missing = Vec::new();
135 for parameter in parameters {
136 let value = if let Some(input) = inputs.get(¶meter.name) {
137 Some(parse_input(parameter, input)?)
138 } else {
139 parameter.default.clone()
140 };
141 if let Some(value) = value {
142 values.insert(parameter.name.clone(), value);
143 } else {
144 missing.push(Diagnostic {
145 severity: Severity::Error,
146 span: parameter.span,
147 message: format!(
148 "missing required configuration parameter `{}` ({})",
149 parameter.name, parameter.type_name
150 ),
151 help: vec![format!(
152 "supply --config {}=<{}> for this package",
153 parameter.name, parameter.type_name
154 )],
155 notes: Vec::new(),
156 });
157 }
158 }
159 if !missing.is_empty() {
160 return Err(missing.into());
161 }
162 Ok(values)
163}
164
165pub(crate) fn resolved_tokens(
166 source: &str,
167 file: FileId,
168 inputs: &BTreeMap<String, String>,
169) -> Result<Vec<Token>, CompileError> {
170 let parameters = parameters(source, file)?;
171 let values = resolve(¶meters, inputs, file)?;
172 let mut tokens = Vec::new();
173 for parameter in parameters {
174 tokens.extend(parameter.header);
175 tokens.push(Token::new(TokenKind::Equals, parameter.value_span));
176 let value = values.get(¶meter.name).ok_or_else(|| {
177 error(
178 parameter.span,
179 "configuration resolution omitted a parameter".into(),
180 )
181 })?;
182 tokens.push(Token::new(value.token(), parameter.value_span));
183 tokens.push(Token::new(TokenKind::Semicolon, parameter.value_span));
184 }
185 tokens.push(Token::new(
186 TokenKind::Eof,
187 Span {
188 file,
189 start: source.len() as u32,
190 end: source.len() as u32,
191 },
192 ));
193 Ok(tokens)
194}
195
196fn parse_input(parameter: &ConfigParameter, input: &str) -> Result<ConfigValue, CompileError> {
197 let invalid = || {
198 error(
199 parameter.span,
200 format!(
201 "configuration parameter `{}` requires {}; supply --config {}=<{}>",
202 parameter.name, parameter.type_name, parameter.name, parameter.type_name
203 ),
204 )
205 };
206 match parameter.type_name {
207 "string" => Ok(ConfigValue::String(string_value::encode(input))),
208 "number" => input
209 .parse::<f64>()
210 .ok()
211 .filter(|value| value.is_finite())
212 .map(ConfigValue::Number)
213 .ok_or_else(invalid),
214 "boolean" => input
215 .parse::<bool>()
216 .map(ConfigValue::Boolean)
217 .map_err(|_| invalid()),
218 _ => Err(invalid()),
219 }
220}
221
222fn error(span: Span, message: String) -> CompileError {
223 vec![Diagnostic { severity: Severity::Error, span, message, help: vec!["src/config.ts accepts only exported const primitive literals and exported declare const primitive types".into()], notes: Vec::new() }].into()
224}
225
226struct ConfigReader<'a> {
227 source: &'a str,
228 tokens: &'a [Token],
229 pos: usize,
230}
231
232impl ConfigReader<'_> {
233 fn parameter(&mut self) -> Result<ConfigParameter, CompileError> {
234 let export = self.expect(&TokenKind::Export)?;
235 let declared = self.text(self.peek()) == "declare";
236 if declared {
237 self.advance();
238 }
239 let constant = self.expect(&TokenKind::Const)?;
240 let name = self.expect(&TokenKind::Identifier)?;
241 let mut header = vec![export, constant, name.clone()];
242 let annotation = if self.peek().kind == TokenKind::Colon {
243 header.push(self.advance());
244 let token = self.expect(&TokenKind::Identifier)?;
245 let type_name = match self.text(&token) {
246 "string" => "string",
247 "number" => "number",
248 "boolean" => "boolean",
249 _ => {
250 return Err(error(
251 token.span,
252 "configuration types must be string, number, or boolean".into(),
253 ));
254 }
255 };
256 header.push(token);
257 Some(type_name)
258 } else {
259 None
260 };
261 let value_span = self.peek().span;
262 let default = if declared {
263 if annotation.is_none() {
264 return Err(error(
265 name.span,
266 "required configuration needs an explicit primitive type".into(),
267 ));
268 }
269 None
270 } else {
271 self.expect(&TokenKind::Equals)?;
272 let negative = self.take(&TokenKind::Minus);
273 let literal = self.advance();
274 let value = match literal.kind {
275 TokenKind::StringLiteral(value) | TokenKind::TemplateNoSubstitution(value)
276 if !negative =>
277 {
278 ConfigValue::String(value)
279 }
280 TokenKind::NumberLiteral(value) if value.is_finite() => {
281 ConfigValue::Number(if negative { -value } else { value })
282 }
283 TokenKind::BooleanLiteral(value) if !negative => ConfigValue::Boolean(value),
284 _ => {
285 return Err(error(
286 literal.span,
287 "configuration defaults must be primitive literals".into(),
288 ));
289 }
290 };
291 if annotation.is_some_and(|name| name != value.type_name()) {
292 return Err(error(
293 literal.span,
294 "configuration default does not match its declared type".into(),
295 ));
296 }
297 Some(value)
298 };
299 if self.peek().kind != TokenKind::Eof {
300 self.expect(&TokenKind::Semicolon)?;
301 }
302 let type_name = annotation
303 .or_else(|| default.as_ref().map(ConfigValue::type_name))
304 .ok_or_else(|| {
305 error(
306 name.span,
307 "configuration parameter needs a type or default".into(),
308 )
309 })?;
310 Ok(ConfigParameter {
311 name: self.text(&name).to_owned(),
312 type_name,
313 default,
314 span: name.span,
315 header,
316 value_span,
317 })
318 }
319
320 fn peek(&self) -> &Token {
321 &self.tokens[self.pos]
323 }
324
325 fn advance(&mut self) -> Token {
326 let token = self.peek().clone();
327 if token.kind != TokenKind::Eof {
328 self.pos += 1;
329 }
330 token
331 }
332
333 fn take(&mut self, kind: &TokenKind) -> bool {
334 if &self.peek().kind != kind {
335 return false;
336 }
337 self.advance();
338 true
339 }
340
341 fn expect(&mut self, kind: &TokenKind) -> Result<Token, CompileError> {
342 if &self.peek().kind != kind {
343 return Err(error(
344 self.peek().span,
345 format!("invalid configuration declaration: expected {kind:?}"),
346 ));
347 }
348 Ok(self.advance())
349 }
350
351 fn text(&self, token: &Token) -> &str {
352 &self.source[token.span.start as usize..token.span.end as usize]
354 }
355}
356
357mod string_value {
360 use crate::literal_units::{literal_units, push_literal_char, push_lone_surrogate};
361 use serde::{Deserialize, Deserializer, Serialize, Serializer};
362
363 #[derive(Serialize, Deserialize)]
364 #[serde(untagged)]
365 enum Wire {
366 Text(String),
367 Units { utf16: Vec<u16> },
368 }
369
370 pub fn encode(text: &str) -> String {
371 let mut literal = String::new();
372 for character in text.chars() {
373 push_literal_char(&mut literal, character);
374 }
375 literal
376 }
377
378 pub fn serialize<S: Serializer>(text: &str, serializer: S) -> Result<S::Ok, S::Error> {
379 let units = literal_units(text);
380 match String::from_utf16(&units) {
381 Ok(text) => Wire::Text(text).serialize(serializer),
382 Err(_) => Wire::Units { utf16: units }.serialize(serializer),
383 }
384 }
385
386 pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<String, D::Error> {
387 match Wire::deserialize(deserializer)? {
388 Wire::Text(text) => Ok(encode(&text)),
389 Wire::Units { utf16 } => {
390 let mut literal = String::new();
391 for character in char::decode_utf16(utf16) {
392 match character {
393 Ok(character) => push_literal_char(&mut literal, character),
394 Err(error) => {
395 push_lone_surrogate(
396 &mut literal,
397 u32::from(error.unpaired_surrogate()),
398 );
399 }
400 }
401 }
402 Ok(literal)
403 }
404 }
405 }
406}
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411
412 #[test]
413 fn defaults_required_values_and_primitive_validation() {
414 let source = "// Configuration\nexport declare const url: string;\nexport const timeout = -5000;\nexport const enabled: boolean = false;\nexport const title = `hello`;";
415 let parameters = parameters(source, FileId(0)).unwrap();
416 assert!(resolve(¶meters, &BTreeMap::new(), FileId(0)).is_err());
417 let inputs = BTreeMap::from([
418 ("url".into(), "https://example.test/?a=b".into()),
419 ("timeout".into(), "0.5".into()),
420 ("enabled".into(), "true".into()),
421 ]);
422 let values = resolve(¶meters, &inputs, FileId(0)).unwrap();
423 assert_eq!(
424 values["url"],
425 ConfigValue::String("https://example.test/?a=b".into())
426 );
427 assert_eq!(values["enabled"], ConfigValue::Boolean(true));
428 let tokens = resolved_tokens(source, FileId(0), &inputs).unwrap();
429 let (_, diagnostics) = crate::parser::parse_checked(source, tokens, FileId(0)).unwrap();
430 assert!(diagnostics.is_empty(), "{diagnostics:?}");
431 for (name, value) in [
432 ("timeout", "NaN"),
433 ("timeout", "inf"),
434 ("enabled", "yes"),
435 ("unknown", "value"),
436 ] {
437 let mut invalid = inputs.clone();
438 invalid.insert(name.into(), value.into());
439 assert!(
440 resolve(¶meters, &invalid, FileId(0)).is_err(),
441 "{name}={value}"
442 );
443 }
444 }
445
446 #[test]
447 fn rejects_executable_configuration_and_conflicting_declarations() {
448 for source in [
449 "export let x = 1;",
450 "const x = 1;",
451 "export const x = 1 + 2;",
452 "export const x = Math.random();",
453 "export const x = {};",
454 "export declare const x;",
455 "export declare const x: string | undefined;",
456 "export const x: number = 'text';",
457 "export const x = 1; export const x = 2;",
458 "import { x } from './other';",
459 "export function x(): number { return 1; }",
460 ] {
461 assert!(parameters(source, FileId(0)).is_err(), "{source}");
462 }
463 }
464}
465
466#[cfg(test)]
467mod representation_tests {
468 use super::*;
469
470 #[test]
471 fn signed_zero_survives_defaults_overrides_and_metadata() {
472 let parameters = parameters("export const value = -0;", FileId(0)).unwrap();
473 for inputs in [
474 BTreeMap::new(),
475 BTreeMap::from([("value".into(), "-0".into())]),
476 ] {
477 let values = resolve(¶meters, &inputs, FileId(0)).unwrap();
478 assert_eq!(values["value"], ConfigValue::Number(-0.0));
479 assert_ne!(values["value"], ConfigValue::Number(0.0));
480 let text = serde_json::to_string(&values).unwrap();
481 let decoded: BTreeMap<String, ConfigValue> = serde_json::from_str(&text).unwrap();
482 assert_eq!(decoded, values);
483 }
484 }
485
486 #[test]
487 fn marker_characters_and_lone_surrogates_roundtrip_without_loss() {
488 let source = "export const text = '\\ud800';";
489 let parameters = parameters(source, FileId(0)).unwrap();
490 let default = resolve(¶meters, &BTreeMap::new(), FileId(0)).unwrap();
491 let encoded = serde_json::to_string(&default).unwrap();
492 assert!(encoded.contains("utf16"));
493 let decoded: BTreeMap<String, ConfigValue> = serde_json::from_str(&encoded).unwrap();
494 assert_eq!(decoded, default);
495 let input = "\u{10fffe}\u{100000}";
496 let values = resolve(
497 ¶meters,
498 &BTreeMap::from([("text".into(), input.into())]),
499 FileId(0),
500 )
501 .unwrap();
502 let ConfigValue::String(text) = &values["text"] else {
503 panic!("string");
504 };
505 assert_eq!(
506 crate::literal_units::literal_units(text),
507 input.encode_utf16().collect::<Vec<_>>()
508 );
509 assert_eq!(serde_json::to_value(&values).unwrap()["text"], input);
510 let decoded: BTreeMap<String, ConfigValue> =
511 serde_json::from_value(serde_json::to_value(&values).unwrap()).unwrap();
512 assert_eq!(decoded, values);
513 }
514}