Skip to content

Commit

Permalink
Add %* format specifier (#619)
Browse files Browse the repository at this point in the history
RFC: #165
  • Loading branch information
Kampfkarren authored Aug 4, 2022
1 parent 2c12bad commit 4658219
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 3 deletions.
2 changes: 1 addition & 1 deletion Analysis/src/Linter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1433,7 +1433,7 @@ class LintFormatString : AstVisitor
const char* checkStringFormat(const char* data, size_t size)
{
const char* flags = "-+ #0";
const char* options = "cdiouxXeEfgGqs";
const char* options = "cdiouxXeEfgGqs*";

for (size_t i = 0; i < size; ++i)
{
Expand Down
6 changes: 4 additions & 2 deletions Analysis/src/TypeVar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,7 @@ ConstrainedTypeVarIterator end(const ConstrainedTypeVar* ctv)

static std::vector<TypeId> parseFormatString(TypeChecker& typechecker, const char* data, size_t size)
{
const char* options = "cdiouxXeEfgGqs";
const char* options = "cdiouxXeEfgGqs*";

std::vector<TypeId> result;

Expand All @@ -1072,14 +1072,16 @@ static std::vector<TypeId> parseFormatString(TypeChecker& typechecker, const cha
continue;

// we just ignore all characters (including flags/precision) up until first alphabetic character
while (i < size && !(data[i] > 0 && isalpha(data[i])))
while (i < size && !(data[i] > 0 && (isalpha(data[i]) || data[i] == '*')))
i++;

if (i == size)
break;

if (data[i] == 'q' || data[i] == 's')
result.push_back(typechecker.stringType);
else if (data[i] == '*')
result.push_back(typechecker.unknownType);
else if (strchr(options, data[i]))
result.push_back(typechecker.numberType);
else
Expand Down
22 changes: 22 additions & 0 deletions VM/src/lstrlib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#include <string.h>
#include <stdio.h>

LUAU_FASTFLAGVARIABLE(LuauTostringFormatSpecifier, false);

/* macro to `unsign' a character */
#define uchar(c) ((unsigned char)(c))

Expand Down Expand Up @@ -1032,6 +1034,26 @@ static int str_format(lua_State* L)
break;
}
}
case '*':
{
if (!FFlag::LuauTostringFormatSpecifier)
{
luaL_error(L, "invalid option '%%*' to 'format'");
break;
}

if (formatItemSize != 1)
{
luaL_error(L, "'%%*' does not take a form");
}

size_t length;
const char* string = luaL_tolstring(L, arg, &length);

luaL_addlstring(&b, string, length);

continue; /* skip the `addsize' at the end */
}
default:
{ /* also treat cases `pnLlh' */
luaL_error(L, "invalid option '%%%c' to 'format'", *(strfrmt - 1));
Expand Down
2 changes: 2 additions & 0 deletions tests/Conformance.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ TEST_CASE("Clear")

TEST_CASE("Strings")
{
ScopedFastFlag sff{"LuauTostringFormatSpecifier", true};

runConformance("strings.lua");
}

Expand Down
23 changes: 23 additions & 0 deletions tests/TypeInfer.builtins.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,29 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_correctly_ordered_types")
CHECK_EQ(tm->givenType, typeChecker.numberType);
}

TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_tostring_specifier")
{
CheckResult result = check(R"(
--!strict
string.format("%* %* %* %*", "string", 1, true, function() end)
)");

LUAU_REQUIRE_NO_ERRORS(result);
}

TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_tostring_specifier_type_constraint")
{
CheckResult result = check(R"(
local function f(x): string
local _ = string.format("%*", x)
return x
end
)");

LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(string) -> string", toString(requireType("f")));
}

TEST_CASE_FIXTURE(BuiltinsFixture, "xpcall")
{
CheckResult result = check(R"(
Expand Down
20 changes: 20 additions & 0 deletions tests/conformance/strings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,26 @@ assert(string.format('"-%20s.20s"', string.rep("%", 2000)) ==
-- longest number that can be formated
assert(string.len(string.format('%99.99f', -1e308)) >= 100)

local function return_one_thing()
return "hi"
end
local function return_two_nils()
return nil, nil
end

assert(string.format("%*", return_one_thing()) == "hi")
assert(string.format("%* %*", return_two_nils()) == "nil nil")
assert(pcall(function()
string.format("%* %* %*", return_two_nils())
end) == false)

assert(string.format("%*", "a\0b\0c") == "a\0b\0c")
assert(string.format("%*", string.rep("doge", 3000)) == string.rep("doge", 3000))

assert(pcall(function()
string.format("%#*", "bad form")
end) == false)

assert(loadstring("return 1\n--comentário sem EOL no final")() == 1)


Expand Down

0 comments on commit 4658219

Please sign in to comment.