LiteralString Type¶
LiteralString is a compile-time type that restricts function parameters to accept only string literals known at compile time. Inspired by Python PEP 675, it helps prevent injection vulnerabilities by ensuring that security-sensitive strings are not constructed from user input.
Usage¶
Annotate a parameter with LiteralString to require a string literal at the call site:
def safe_query(query: LiteralString) -> str:
return f"executing: {query}"
# OK: string literal
result = safe_query("SELECT * FROM users")
# ERROR: runtime string variable
user_input: str = "DROP TABLE users"
result = safe_query(user_input) # Cannot pass 'str' to 'LiteralString'
String Literal Concatenation¶
Concatenation of string literals produces a LiteralString:
def safe_query(query: LiteralString) -> str:
return f"executing: {query}"
# OK: concatenation of literals is still a LiteralString
result = safe_query("SELECT * " + "FROM users")
result2 = safe_query("A" + "B" + "C")
Accepted Forms¶
An argument is a LiteralString when it is a string literal, a + concatenation whose operands are
themselves accepted forms, or either of those wrapped in redundant parentheses — parentheses never
change meaning (the canonical-form contract, #1170):
def safe_query(query: LiteralString) -> str:
return f"executing: {query}"
def main():
print(safe_query(("SELECT * FROM users"))) # executing: SELECT * FROM users
print(safe_query(("SELECT * ") + ("FROM users")))
PEP 675 also treats "a" * 3, an f-string with literal-only holes, and implicit concatenation
"a" "b" as LiteralString; Sharpy deliberately does not accept those forms today (the first
two are refused as str, the third does not parse). Widening is a separate decision.
Store Positions¶
A literal-derived string is accepted at every store position where the slot is
LiteralString — the same scope as integer constant conversion:
class Config:
key: LiteralString = "default" # field declaration
def query(sql: LiteralString) -> str:
return sql
def run(sql: LiteralString = "SELECT 1") -> str: # parameter default
return sql
def make() -> LiteralString:
return "SELECT 1" # return
def gen() -> LiteralString:
yield "a" # yield
def main() -> None:
x: LiteralString = "hello" # declaration
x = "world" # plain store
print(x) # world
c: Config = Config()
c.key = "k" # attribute store
print(c.key) # k
xs: list[LiteralString] = ["a", "b"] # collection-literal elements
xs[0] = "z" # index store
print(xs[0]) # z
d: dict[str, LiteralString] = {}
d["k"] = "v" # dict-value store
print(d["k"]) # v
print(query("SELECT 1")) # positional argument -> SELECT 1
print(query(sql="SELECT 1")) # keyword argument -> SELECT 1
print(run()) # default -> SELECT 1
print(make()) # return -> SELECT 1
for s in gen():
print(s) # yield -> a
t: tuple[LiteralString, int] = ("a", 1) # tuple element
print(t[0]) # a
print(query((x := "walrus"))) # walrus -> walrus
f: () -> LiteralString = lambda: "b" # lambda body under a typed target
print(f()) # b
r: LiteralString = "a" if True else "c" # conditional of literals
print(r) # a
s2: LiteralString = "a"
s2 += "b" # augmented
print(s2) # ab
The expression's type stays str; LiteralString is the slot's declared type.
A str variable is always refused — the literal-derived check is a compile-time
fact, not a type.
Refused forms: f-strings (f"..." — interpolation is runtime), "a" * 3, and any
non-literal str expression. See #1741 for the full forms table.
Type Relationship¶
LiteralString is a subtype of str:
- A
LiteralStringvalue can be used anywhere astris expected - A
strvalue cannot be used where aLiteralStringis expected
This ensures that functions accepting str work with literal strings, but functions requiring LiteralString reject runtime-constructed strings. LiteralString can appear as the payload of T? and T | None:
Use Surface¶
A LiteralString value supports every operation a str does: operators (==, !=, <,
+, *), comparison chains, len, indexing, slicing, iteration, in, truthiness (if x:),
method calls (including split, which returns list[str]), f-strings, str(), and sorted.
def main() -> None:
x: LiteralString = "hello"
print(x.upper()) # HELLO
print(x.startswith("he")) # True
print(x.replace("l", "r", 1)) # herlo
print(f"{x}!") # hello!
print(str(x)) # hello
x + "b" is still literal-derived (admissible into a LiteralString slot), while x + s
(where s: str) is not — x += s is SPY0220.
Use Cases¶
LiteralString is primarily useful for:
- SQL queries — prevent SQL injection
- Shell commands — prevent command injection
- Regular expressions — ensure patterns are compile-time constants
- Configuration keys — ensure keys match known constants
def execute_sql(query: LiteralString) -> None:
...
def run_command(cmd: LiteralString) -> None:
...
def compile_regex(pattern: LiteralString) -> None:
...
Generated C¶
LiteralString has no runtime representation — it emits as string in C#. The compile-time check is performed entirely during type checking:
generates:
Diagnostics¶
When a non-literal str is passed to a LiteralString parameter, the compiler emits a type error:
Implementation
- ✅ Implemented — LiteralStringType singleton in SemanticType.cs, resolved in TypeResolver.cs
- Subtyping: LiteralStringType.IsAssignableTo(str) returns true
- Concatenation: literal + literal preserves LiteralString type
- Emits as string in C# (no runtime distinction)