From d4c2c64dcd8a7fa4fcc250ca141882bcb392766c Mon Sep 17 00:00:00 2001 From: Alexander Youngblood Date: Fri, 28 Mar 2025 10:07:23 -0700 Subject: [PATCH] Sync to upstream/release/667 --- Analysis/include/Luau/AnyTypeSummary.h | 148 --- Analysis/include/Luau/Constraint.h | 1 + Analysis/include/Luau/ConstraintSolver.h | 2 +- Analysis/include/Luau/DataFlowGraph.h | 6 - Analysis/include/Luau/FragmentAutocomplete.h | 27 +- Analysis/include/Luau/Frontend.h | 2 - Analysis/include/Luau/Generalization.h | 3 +- Analysis/include/Luau/Module.h | 8 +- Analysis/include/Luau/NonStrictTypeChecker.h | 3 +- Analysis/include/Luau/Type.h | 11 +- Analysis/include/Luau/TypeFunction.h | 1 + Analysis/src/AnyTypeSummary.cpp | 902 --------------- Analysis/src/AstJsonEncoder.cpp | 2 + Analysis/src/ConstraintGenerator.cpp | 68 +- Analysis/src/ConstraintSolver.cpp | 153 ++- Analysis/src/DataFlowGraph.cpp | 6 - Analysis/src/EmbeddedBuiltinDefinitions.cpp | 13 +- Analysis/src/FragmentAutocomplete.cpp | 431 +++++++- Analysis/src/Frontend.cpp | 20 +- Analysis/src/Generalization.cpp | 68 +- Analysis/src/Linter.cpp | 183 ++- Analysis/src/Module.cpp | 2 +- Analysis/src/NonStrictTypeChecker.cpp | 1 + Analysis/src/Normalize.cpp | 40 +- Analysis/src/Substitution.cpp | 3 + Analysis/src/Subtyping.cpp | 52 +- Analysis/src/TableLiteralInference.cpp | 117 +- Analysis/src/Transpiler.cpp | 151 ++- Analysis/src/TypeAttach.cpp | 4 +- Analysis/src/TypeChecker2.cpp | 16 +- Analysis/src/TypeFunction.cpp | 428 +++++++- Analysis/src/TypeFunctionRuntime.cpp | 6 +- Analysis/src/Unifier2.cpp | 55 +- Ast/include/Luau/Ast.h | 22 +- Ast/include/Luau/Cst.h | 36 +- Ast/include/Luau/Parser.h | 12 +- Ast/src/Ast.cpp | 40 +- Ast/src/Cst.cpp | 23 +- Ast/src/Parser.cpp | 910 ++++++++++----- Sources.cmake | 3 - fuzz/proto.cpp | 1 + tests/AnyTypeSummary.test.cpp | 1038 ------------------ tests/AstJsonEncoder.test.cpp | 5 +- tests/Autocomplete.test.cpp | 24 + tests/Fixture.cpp | 2 +- tests/FragmentAutocomplete.test.cpp | 964 +++++++++++++++- tests/Linter.test.cpp | 321 ++++++ tests/Normalize.test.cpp | 62 +- tests/Parser.test.cpp | 54 +- tests/ScopedFlags.h | 4 +- tests/Transpiler.test.cpp | 337 ++++-- tests/TypeFunction.test.cpp | 45 +- tests/TypeFunction.user.test.cpp | 105 +- tests/TypeInfer.aliases.test.cpp | 12 +- tests/TypeInfer.functions.test.cpp | 27 + tests/TypeInfer.provisional.test.cpp | 8 +- tests/TypeInfer.refinements.test.cpp | 23 +- tests/TypeInfer.singletons.test.cpp | 21 + tests/TypeInfer.tables.test.cpp | 146 ++- tests/TypeInfer.test.cpp | 12 +- 60 files changed, 4327 insertions(+), 2863 deletions(-) delete mode 100644 Analysis/include/Luau/AnyTypeSummary.h delete mode 100644 Analysis/src/AnyTypeSummary.cpp delete mode 100644 tests/AnyTypeSummary.test.cpp diff --git a/Analysis/include/Luau/AnyTypeSummary.h b/Analysis/include/Luau/AnyTypeSummary.h deleted file mode 100644 index d99eea84..00000000 --- a/Analysis/include/Luau/AnyTypeSummary.h +++ /dev/null @@ -1,148 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#pragma once - -#include "Luau/AstQuery.h" -#include "Luau/Config.h" -#include "Luau/ModuleResolver.h" -#include "Luau/Scope.h" -#include "Luau/Variant.h" -#include "Luau/Normalize.h" -#include "Luau/TypePack.h" -#include "Luau/TypeArena.h" - -#include -#include -#include -#include - -namespace Luau -{ - -class AstStat; -class ParseError; -struct TypeError; -struct LintWarning; -struct GlobalTypes; -struct ModuleResolver; -struct ParseResult; -struct DcrLogger; - -struct TelemetryTypePair -{ - std::string annotatedType; - std::string inferredType; -}; - -struct AnyTypeSummary -{ - TypeArena arena; - - AstStatBlock* rootSrc = nullptr; - DenseHashSet seenTypeFamilyInstances{nullptr}; - - int recursionCount = 0; - - std::string root; - int strictCount = 0; - - DenseHashMap seen{nullptr}; - - AnyTypeSummary(); - - void traverse(const Module* module, AstStat* src, NotNull builtinTypes); - - std::pair checkForAnyCast(const Scope* scope, AstExprTypeAssertion* expr); - - bool containsAny(TypePackId typ); - bool containsAny(TypeId typ); - - bool isAnyCast(const Scope* scope, AstExpr* expr, const Module* module, NotNull builtinTypes); - bool isAnyCall(const Scope* scope, AstExpr* expr, const Module* module, NotNull builtinTypes); - - bool hasVariadicAnys(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull builtinTypes); - bool hasArgAnys(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull builtinTypes); - bool hasAnyReturns(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull builtinTypes); - - TypeId checkForFamilyInhabitance(const TypeId instance, Location location); - TypeId lookupType(const AstExpr* expr, const Module* module, NotNull builtinTypes); - TypePackId reconstructTypePack(const AstArray exprs, const Module* module, NotNull builtinTypes); - - DenseHashSet seenTypeFunctionInstances{nullptr}; - TypeId lookupAnnotation(AstType* annotation, const Module* module, NotNull builtintypes); - std::optional lookupPackAnnotation(AstTypePack* annotation, const Module* module); - TypeId checkForTypeFunctionInhabitance(const TypeId instance, const Location location); - - enum Pattern : uint64_t - { - Casts, - FuncArg, - FuncRet, - FuncApp, - VarAnnot, - VarAny, - TableProp, - Alias, - Assign, - TypePk - }; - - struct TypeInfo - { - Pattern code; - std::string node; - TelemetryTypePair type; - - explicit TypeInfo(Pattern code, std::string node, TelemetryTypePair type); - }; - - struct FindReturnAncestry final : public AstVisitor - { - AstNode* currNode{nullptr}; - AstNode* stat{nullptr}; - Position rootEnd; - bool found = false; - - explicit FindReturnAncestry(AstNode* stat, Position rootEnd); - - bool visit(AstType* node) override; - bool visit(AstNode* node) override; - bool visit(AstStatFunction* node) override; - bool visit(AstStatLocalFunction* node) override; - }; - - std::vector typeInfo; - - /** - * Fabricates a scope that is a child of another scope. - * @param node the lexical node that the scope belongs to. - * @param parent the parent scope of the new scope. Must not be null. - */ - const Scope* childScope(const AstNode* node, const Scope* parent); - - std::optional matchRequire(const AstExprCall& call); - AstNode* getNode(AstStatBlock* root, AstNode* node); - const Scope* findInnerMostScope(const Location location, const Module* module); - const AstNode* findAstAncestryAtLocation(const AstStatBlock* root, AstNode* node); - - void visit(const Scope* scope, AstStat* stat, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatBlock* block, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatIf* ifStatement, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatWhile* while_, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatRepeat* repeat, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatReturn* ret, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatLocal* local, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatFor* for_, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatForIn* forIn, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatAssign* assign, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatCompoundAssign* assign, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatFunction* function, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatLocalFunction* function, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatTypeAlias* alias, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatExpr* expr, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatDeclareGlobal* declareGlobal, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatDeclareClass* declareClass, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatDeclareFunction* declareFunction, const Module* module, NotNull builtinTypes); - void visit(const Scope* scope, AstStatError* error, const Module* module, NotNull builtinTypes); -}; - -} // namespace Luau \ No newline at end of file diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index 2b0fbeb7..1499802d 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -50,6 +50,7 @@ struct GeneralizationConstraint TypeId sourceType; std::vector interiorTypes; + bool hasDeprecatedAttribute = false; }; // variables ~ iterate iterator diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index f561072d..55d337ff 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -365,7 +365,7 @@ public: * @returns a non-free type that generalizes the argument, or `std::nullopt` if one * does not exist */ - std::optional generalizeFreeType(NotNull scope, TypeId type, bool avoidSealingTables = false); + std::optional generalizeFreeType(NotNull scope, TypeId type); /** * Checks the existing set of constraints to see if there exist any that contain diff --git a/Analysis/include/Luau/DataFlowGraph.h b/Analysis/include/Luau/DataFlowGraph.h index 1f28abe9..8a70332e 100644 --- a/Analysis/include/Luau/DataFlowGraph.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -38,8 +38,6 @@ struct DataFlowGraph DefId getDef(const AstExpr* expr) const; // Look up the definition optionally, knowing it may not be present. std::optional getDefOptional(const AstExpr* expr) const; - // Look up for the rvalue def for a compound assignment. - std::optional getRValueDefForCompoundAssign(const AstExpr* expr) const; DefId getDef(const AstLocal* local) const; @@ -66,10 +64,6 @@ private: // All keys in this maps are really only statements that ambiently declares a symbol. DenseHashMap declaredDefs{nullptr}; - // Compound assignments are in a weird situation where the local being assigned to is also being used at its - // previous type implicitly in an rvalue position. This map provides the previous binding. - DenseHashMap compoundAssignDefs{nullptr}; - DenseHashMap astRefinementKeys{nullptr}; friend struct DataFlowGraphBuilder; }; diff --git a/Analysis/include/Luau/FragmentAutocomplete.h b/Analysis/include/Luau/FragmentAutocomplete.h index 305bc06d..c701271c 100644 --- a/Analysis/include/Luau/FragmentAutocomplete.h +++ b/Analysis/include/Luau/FragmentAutocomplete.h @@ -49,6 +49,8 @@ struct FragmentAutocompleteAncestryResult std::vector localStack; std::vector ancestry; AstStat* nearestStatement = nullptr; + AstStatBlock* parentBlock = nullptr; + Location fragmentSelectionRegion; }; struct FragmentParseResult @@ -59,6 +61,7 @@ struct FragmentParseResult AstStat* nearestStatement = nullptr; std::vector commentLocations; std::unique_ptr alloc = std::make_unique(); + Position scopePos{0, 0}; }; struct FragmentTypeCheckResult @@ -76,10 +79,28 @@ struct FragmentAutocompleteResult AutocompleteResult acResults; }; -FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* root, const Position& cursorPos); +struct FragmentRegion +{ + Location fragmentLocation; + AstStat* nearestStatement = nullptr; // used for tests + AstStatBlock* parentBlock = nullptr; // used for scope detection +}; + +FragmentRegion getFragmentRegion(AstStatBlock* root, const Position& cursorPosition); +FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* stale, const Position& cursorPos, AstStatBlock* lastGoodParse); +FragmentAutocompleteAncestryResult findAncestryForFragmentParse_DEPRECATED(AstStatBlock* root, const Position& cursorPos); + +std::optional parseFragment_DEPRECATED( + AstStatBlock* root, + AstNameTable* names, + std::string_view src, + const Position& cursorPos, + std::optional fragmentEndPosition +); std::optional parseFragment( - AstStatBlock* root, + AstStatBlock* stale, + AstStatBlock* mostRecentParse, AstNameTable* names, std::string_view src, const Position& cursorPos, @@ -93,6 +114,7 @@ std::pair typecheckFragment( std::optional opts, std::string_view src, std::optional fragmentEndPosition, + AstStatBlock* recentParse = nullptr, IFragmentAutocompleteReporter* reporter = nullptr ); @@ -104,6 +126,7 @@ FragmentAutocompleteResult fragmentAutocomplete( std::optional opts, StringCompletionCallback callback, std::optional fragmentEndPosition = std::nullopt, + AstStatBlock* recentParse = nullptr, IFragmentAutocompleteReporter* reporter = nullptr ); diff --git a/Analysis/include/Luau/Frontend.h b/Analysis/include/Luau/Frontend.h index 918019e2..659053d3 100644 --- a/Analysis/include/Luau/Frontend.h +++ b/Analysis/include/Luau/Frontend.h @@ -10,7 +10,6 @@ #include "Luau/Set.h" #include "Luau/TypeCheckLimits.h" #include "Luau/Variant.h" -#include "Luau/AnyTypeSummary.h" #include #include @@ -34,7 +33,6 @@ struct HotComment; struct BuildQueueItem; struct BuildQueueWorkState; struct FrontendCancellationToken; -struct AnyTypeSummary; struct LoadDefinitionFileResult { diff --git a/Analysis/include/Luau/Generalization.h b/Analysis/include/Luau/Generalization.h index 22ebf171..7f20ea2e 100644 --- a/Analysis/include/Luau/Generalization.h +++ b/Analysis/include/Luau/Generalization.h @@ -13,8 +13,7 @@ std::optional generalize( NotNull builtinTypes, NotNull scope, NotNull> cachedTypes, - TypeId ty, - /* avoid sealing tables*/ bool avoidSealingTables = false + TypeId ty ); } diff --git a/Analysis/include/Luau/Module.h b/Analysis/include/Luau/Module.h index 521361cb..82482850 100644 --- a/Analysis/include/Luau/Module.h +++ b/Analysis/include/Luau/Module.h @@ -8,7 +8,6 @@ #include "Luau/ParseResult.h" #include "Luau/Scope.h" #include "Luau/TypeArena.h" -#include "Luau/AnyTypeSummary.h" #include "Luau/DataFlowGraph.h" #include @@ -21,14 +20,13 @@ LUAU_FASTFLAG(LuauIncrementalAutocompleteCommentDetection) namespace Luau { -using LogLuauProc = void (*)(std::string_view); +using LogLuauProc = void (*)(std::string_view, std::string_view); extern LogLuauProc logLuau; void setLogLuau(LogLuauProc ll); void resetLogLuauProc(); struct Module; -struct AnyTypeSummary; using ScopePtr = std::shared_ptr; using ModulePtr = std::shared_ptr; @@ -86,10 +84,6 @@ struct Module TypeArena interfaceTypes; TypeArena internalTypes; - // Summary of Ast Nodes that either contain - // user annotated anys or typechecker inferred anys - AnyTypeSummary ats{}; - // Scopes and AST types refer to parse data, so we need to keep that alive std::shared_ptr allocator; std::shared_ptr names; diff --git a/Analysis/include/Luau/NonStrictTypeChecker.h b/Analysis/include/Luau/NonStrictTypeChecker.h index 880d487f..965a694d 100644 --- a/Analysis/include/Luau/NonStrictTypeChecker.h +++ b/Analysis/include/Luau/NonStrictTypeChecker.h @@ -1,9 +1,10 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/DataFlowGraph.h" +#include "Luau/EqSatSimplification.h" #include "Luau/Module.h" #include "Luau/NotNull.h" -#include "Luau/DataFlowGraph.h" namespace Luau { diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index ced5ed83..8e164232 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -19,7 +19,6 @@ #include #include #include -#include #include LUAU_FASTINT(LuauTableTypeMaximumStringifierLength) @@ -38,6 +37,15 @@ struct Constraint; struct Subtyping; struct TypeChecker2; +enum struct Polarity : uint8_t +{ + None = 0b000, + Positive = 0b001, + Negative = 0b010, + Mixed = 0b011, + Unknown = 0b100, +}; + /** * There are three kinds of type variables: * - `Free` variables are metavariables, which stand for unconstrained types. @@ -396,6 +404,7 @@ struct FunctionType // this flag is used as an optimization to exit early from procedures that manipulate free or generic types. bool hasNoFreeOrGenericTypes = false; bool isCheckedFunction = false; + bool isDeprecatedFunction = false; }; enum class TableState diff --git a/Analysis/include/Luau/TypeFunction.h b/Analysis/include/Luau/TypeFunction.h index bde00461..88f95507 100644 --- a/Analysis/include/Luau/TypeFunction.h +++ b/Analysis/include/Luau/TypeFunction.h @@ -177,6 +177,7 @@ struct FunctionGraphReductionResult DenseHashSet blockedPacks{nullptr}; DenseHashSet reducedTypes{nullptr}; DenseHashSet reducedPacks{nullptr}; + DenseHashSet irreducibleTypes{nullptr}; }; /** diff --git a/Analysis/src/AnyTypeSummary.cpp b/Analysis/src/AnyTypeSummary.cpp deleted file mode 100644 index db50e3e9..00000000 --- a/Analysis/src/AnyTypeSummary.cpp +++ /dev/null @@ -1,902 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/AnyTypeSummary.h" - -#include "Luau/BuiltinDefinitions.h" -#include "Luau/Clone.h" -#include "Luau/Common.h" -#include "Luau/Config.h" -#include "Luau/ConstraintGenerator.h" -#include "Luau/ConstraintSolver.h" -#include "Luau/DataFlowGraph.h" -#include "Luau/DcrLogger.h" -#include "Luau/Module.h" -#include "Luau/Parser.h" -#include "Luau/Scope.h" -#include "Luau/StringUtils.h" -#include "Luau/TimeTrace.h" -#include "Luau/ToString.h" -#include "Luau/Transpiler.h" -#include "Luau/TypeArena.h" -#include "Luau/TypeChecker2.h" -#include "Luau/NonStrictTypeChecker.h" -#include "Luau/TypeInfer.h" -#include "Luau/Variant.h" -#include "Luau/VisitType.h" -#include "Luau/TypePack.h" -#include "Luau/TypeOrPack.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -#include - -LUAU_FASTFLAGVARIABLE(StudioReportLuauAny2); -LUAU_FASTINTVARIABLE(LuauAnySummaryRecursionLimit, 300); - -LUAU_FASTFLAG(DebugLuauMagicTypes); - -namespace Luau -{ - -void AnyTypeSummary::traverse(const Module* module, AstStat* src, NotNull builtinTypes) -{ - visit(findInnerMostScope(src->location, module), src, module, builtinTypes); -} - -void AnyTypeSummary::visit(const Scope* scope, AstStat* stat, const Module* module, NotNull builtinTypes) -{ - RecursionLimiter limiter{&recursionCount, FInt::LuauAnySummaryRecursionLimit}; - - if (auto s = stat->as()) - return visit(scope, s, module, builtinTypes); - else if (auto i = stat->as()) - return visit(scope, i, module, builtinTypes); - else if (auto s = stat->as()) - return visit(scope, s, module, builtinTypes); - else if (auto s = stat->as()) - return visit(scope, s, module, builtinTypes); - else if (auto r = stat->as()) - return visit(scope, r, module, builtinTypes); - else if (auto e = stat->as()) - return visit(scope, e, module, builtinTypes); - else if (auto s = stat->as()) - return visit(scope, s, module, builtinTypes); - else if (auto s = stat->as()) - return visit(scope, s, module, builtinTypes); - else if (auto s = stat->as()) - return visit(scope, s, module, builtinTypes); - else if (auto a = stat->as()) - return visit(scope, a, module, builtinTypes); - else if (auto a = stat->as()) - return visit(scope, a, module, builtinTypes); - else if (auto f = stat->as()) - return visit(scope, f, module, builtinTypes); - else if (auto f = stat->as()) - return visit(scope, f, module, builtinTypes); - else if (auto a = stat->as()) - return visit(scope, a, module, builtinTypes); - else if (auto s = stat->as()) - return visit(scope, s, module, builtinTypes); - else if (auto s = stat->as()) - return visit(scope, s, module, builtinTypes); - else if (auto s = stat->as()) - return visit(scope, s, module, builtinTypes); - else if (auto s = stat->as()) - return visit(scope, s, module, builtinTypes); -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatBlock* block, const Module* module, NotNull builtinTypes) -{ - RecursionCounter counter{&recursionCount}; - - if (recursionCount >= FInt::LuauAnySummaryRecursionLimit) - return; // don't report - - for (AstStat* stat : block->body) - visit(scope, stat, module, builtinTypes); -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatIf* ifStatement, const Module* module, NotNull builtinTypes) -{ - if (ifStatement->thenbody) - { - const Scope* thenScope = findInnerMostScope(ifStatement->thenbody->location, module); - visit(thenScope, ifStatement->thenbody, module, builtinTypes); - } - - if (ifStatement->elsebody) - { - const Scope* elseScope = findInnerMostScope(ifStatement->elsebody->location, module); - visit(elseScope, ifStatement->elsebody, module, builtinTypes); - } -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatWhile* while_, const Module* module, NotNull builtinTypes) -{ - const Scope* whileScope = findInnerMostScope(while_->location, module); - visit(whileScope, while_->body, module, builtinTypes); -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatRepeat* repeat, const Module* module, NotNull builtinTypes) -{ - const Scope* repeatScope = findInnerMostScope(repeat->location, module); - visit(repeatScope, repeat->body, module, builtinTypes); -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatReturn* ret, const Module* module, NotNull builtinTypes) -{ - const Scope* retScope = findInnerMostScope(ret->location, module); - - auto ctxNode = getNode(rootSrc, ret); - bool seenTP = false; - - for (auto val : ret->list) - { - if (isAnyCall(retScope, val, module, builtinTypes)) - { - TelemetryTypePair types; - types.inferredType = toString(lookupType(val, module, builtinTypes)); - TypeInfo ti{Pattern::FuncApp, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - - if (isAnyCast(retScope, val, module, builtinTypes)) - { - if (auto cast = val->as()) - { - TelemetryTypePair types; - - types.annotatedType = toString(lookupAnnotation(cast->annotation, module, builtinTypes)); - types.inferredType = toString(lookupType(cast->expr, module, builtinTypes)); - - TypeInfo ti{Pattern::Casts, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } - - if (ret->list.size > 1 && !seenTP) - { - if (containsAny(retScope->returnType)) - { - seenTP = true; - - TelemetryTypePair types; - - types.inferredType = toString(retScope->returnType); - - TypeInfo ti{Pattern::TypePk, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } - } -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatLocal* local, const Module* module, NotNull builtinTypes) -{ - auto ctxNode = getNode(rootSrc, local); - - TypePackId values = reconstructTypePack(local->values, module, builtinTypes); - auto [head, tail] = flatten(values); - - size_t posn = 0; - for (AstLocal* loc : local->vars) - { - if (local->vars.data[0] == loc && posn < local->values.size) - { - if (loc->annotation) - { - auto annot = lookupAnnotation(loc->annotation, module, builtinTypes); - if (containsAny(annot)) - { - TelemetryTypePair types; - - types.annotatedType = toString(annot); - types.inferredType = toString(lookupType(local->values.data[posn], module, builtinTypes)); - - TypeInfo ti{Pattern::VarAnnot, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } - - const AstExprTypeAssertion* maybeRequire = local->values.data[posn]->as(); - if (!maybeRequire) - continue; - - if (std::min(local->values.size - 1, posn) < head.size()) - { - if (isAnyCast(scope, local->values.data[posn], module, builtinTypes)) - { - TelemetryTypePair types; - - types.inferredType = toString(head[std::min(local->values.size - 1, posn)]); - - TypeInfo ti{Pattern::Casts, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } - } - else - { - - if (std::min(local->values.size - 1, posn) < head.size()) - { - if (loc->annotation) - { - auto annot = lookupAnnotation(loc->annotation, module, builtinTypes); - if (containsAny(annot)) - { - TelemetryTypePair types; - - types.annotatedType = toString(annot); - types.inferredType = toString(head[std::min(local->values.size - 1, posn)]); - - TypeInfo ti{Pattern::VarAnnot, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } - } - else - { - if (tail) - { - if (containsAny(*tail)) - { - TelemetryTypePair types; - - types.inferredType = toString(*tail); - - TypeInfo ti{Pattern::VarAny, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } - } - } - - ++posn; - } -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatFor* for_, const Module* module, NotNull builtinTypes) -{ - const Scope* forScope = findInnerMostScope(for_->location, module); - visit(forScope, for_->body, module, builtinTypes); -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatForIn* forIn, const Module* module, NotNull builtinTypes) -{ - const Scope* loopScope = findInnerMostScope(forIn->location, module); - visit(loopScope, forIn->body, module, builtinTypes); -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatAssign* assign, const Module* module, NotNull builtinTypes) -{ - auto ctxNode = getNode(rootSrc, assign); - - TypePackId values = reconstructTypePack(assign->values, module, builtinTypes); - auto [head, tail] = flatten(values); - - size_t posn = 0; - for (AstExpr* var : assign->vars) - { - TypeId tp = lookupType(var, module, builtinTypes); - if (containsAny(tp)) - { - TelemetryTypePair types; - - types.annotatedType = toString(tp); - - auto loc = std::min(assign->vars.size - 1, posn); - if (head.size() >= assign->vars.size && posn < head.size()) - { - types.inferredType = toString(head[posn]); - } - else if (loc < head.size()) - types.inferredType = toString(head[loc]); - else - types.inferredType = toString(builtinTypes->nilType); - - TypeInfo ti{Pattern::Assign, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - ++posn; - } - - for (AstExpr* val : assign->values) - { - if (isAnyCall(scope, val, module, builtinTypes)) - { - TelemetryTypePair types; - - types.inferredType = toString(lookupType(val, module, builtinTypes)); - - TypeInfo ti{Pattern::FuncApp, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - - if (isAnyCast(scope, val, module, builtinTypes)) - { - if (auto cast = val->as()) - { - TelemetryTypePair types; - - types.annotatedType = toString(lookupAnnotation(cast->annotation, module, builtinTypes)); - types.inferredType = toString(lookupType(val, module, builtinTypes)); - - TypeInfo ti{Pattern::Casts, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } - } - - if (tail) - { - if (containsAny(*tail)) - { - TelemetryTypePair types; - - types.inferredType = toString(*tail); - - TypeInfo ti{Pattern::Assign, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatCompoundAssign* assign, const Module* module, NotNull builtinTypes) -{ - auto ctxNode = getNode(rootSrc, assign); - - TelemetryTypePair types; - - types.inferredType = toString(lookupType(assign->value, module, builtinTypes)); - types.annotatedType = toString(lookupType(assign->var, module, builtinTypes)); - - if (module->astTypes.contains(assign->var)) - { - if (containsAny(*module->astTypes.find(assign->var))) - { - TypeInfo ti{Pattern::Assign, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } - else if (module->astTypePacks.contains(assign->var)) - { - if (containsAny(*module->astTypePacks.find(assign->var))) - { - TypeInfo ti{Pattern::Assign, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } - - if (isAnyCall(scope, assign->value, module, builtinTypes)) - { - TypeInfo ti{Pattern::FuncApp, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - - if (isAnyCast(scope, assign->value, module, builtinTypes)) - { - if (auto cast = assign->value->as()) - { - types.annotatedType = toString(lookupAnnotation(cast->annotation, module, builtinTypes)); - types.inferredType = toString(lookupType(cast->expr, module, builtinTypes)); - - TypeInfo ti{Pattern::Casts, toString(ctxNode), types}; - typeInfo.push_back(ti); - } - } -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatFunction* function, const Module* module, NotNull builtinTypes) -{ - TelemetryTypePair types; - types.inferredType = toString(lookupType(function->func, module, builtinTypes)); - - if (hasVariadicAnys(scope, function->func, module, builtinTypes)) - { - TypeInfo ti{Pattern::VarAny, toString(function), types}; - typeInfo.push_back(ti); - } - - if (hasArgAnys(scope, function->func, module, builtinTypes)) - { - TypeInfo ti{Pattern::FuncArg, toString(function), types}; - typeInfo.push_back(ti); - } - - if (hasAnyReturns(scope, function->func, module, builtinTypes)) - { - TypeInfo ti{Pattern::FuncRet, toString(function), types}; - typeInfo.push_back(ti); - } - - if (function->func->body->body.size > 0) - visit(scope, function->func->body, module, builtinTypes); -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatLocalFunction* function, const Module* module, NotNull builtinTypes) -{ - TelemetryTypePair types; - - if (hasVariadicAnys(scope, function->func, module, builtinTypes)) - { - types.inferredType = toString(lookupType(function->func, module, builtinTypes)); - TypeInfo ti{Pattern::VarAny, toString(function), types}; - typeInfo.push_back(ti); - } - - if (hasArgAnys(scope, function->func, module, builtinTypes)) - { - types.inferredType = toString(lookupType(function->func, module, builtinTypes)); - TypeInfo ti{Pattern::FuncArg, toString(function), types}; - typeInfo.push_back(ti); - } - - if (hasAnyReturns(scope, function->func, module, builtinTypes)) - { - types.inferredType = toString(lookupType(function->func, module, builtinTypes)); - TypeInfo ti{Pattern::FuncRet, toString(function), types}; - typeInfo.push_back(ti); - } - - if (function->func->body->body.size > 0) - visit(scope, function->func->body, module, builtinTypes); -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatTypeAlias* alias, const Module* module, NotNull builtinTypes) -{ - auto ctxNode = getNode(rootSrc, alias); - - auto annot = lookupAnnotation(alias->type, module, builtinTypes); - if (containsAny(annot)) - { - // no expr => no inference for aliases - TelemetryTypePair types; - - types.annotatedType = toString(annot); - TypeInfo ti{Pattern::Alias, toString(ctxNode), types}; - typeInfo.push_back(ti); - } -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatExpr* expr, const Module* module, NotNull builtinTypes) -{ - auto ctxNode = getNode(rootSrc, expr); - - if (isAnyCall(scope, expr->expr, module, builtinTypes)) - { - TelemetryTypePair types; - types.inferredType = toString(lookupType(expr->expr, module, builtinTypes)); - - TypeInfo ti{Pattern::FuncApp, toString(ctxNode), types}; - typeInfo.push_back(ti); - } -} - -void AnyTypeSummary::visit(const Scope* scope, AstStatDeclareGlobal* declareGlobal, const Module* module, NotNull builtinTypes) {} - -void AnyTypeSummary::visit(const Scope* scope, AstStatDeclareClass* declareClass, const Module* module, NotNull builtinTypes) {} - -void AnyTypeSummary::visit(const Scope* scope, AstStatDeclareFunction* declareFunction, const Module* module, NotNull builtinTypes) {} - -void AnyTypeSummary::visit(const Scope* scope, AstStatError* error, const Module* module, NotNull builtinTypes) {} - -TypeId AnyTypeSummary::checkForFamilyInhabitance(const TypeId instance, const Location location) -{ - if (seenTypeFamilyInstances.find(instance)) - return instance; - - seenTypeFamilyInstances.insert(instance); - return instance; -} - -TypeId AnyTypeSummary::lookupType(const AstExpr* expr, const Module* module, NotNull builtinTypes) -{ - const TypeId* ty = module->astTypes.find(expr); - if (ty) - return checkForFamilyInhabitance(follow(*ty), expr->location); - - const TypePackId* tp = module->astTypePacks.find(expr); - if (tp) - { - if (auto fst = first(*tp, /*ignoreHiddenVariadics*/ false)) - return checkForFamilyInhabitance(*fst, expr->location); - else if (finite(*tp) && size(*tp) == 0) - return checkForFamilyInhabitance(builtinTypes->nilType, expr->location); - } - - return builtinTypes->errorRecoveryType(); -} - -TypePackId AnyTypeSummary::reconstructTypePack(AstArray exprs, const Module* module, NotNull builtinTypes) -{ - if (exprs.size == 0) - return arena.addTypePack(TypePack{{}, std::nullopt}); - - std::vector head; - - for (size_t i = 0; i < exprs.size - 1; ++i) - { - head.push_back(lookupType(exprs.data[i], module, builtinTypes)); - } - - const TypePackId* tail = module->astTypePacks.find(exprs.data[exprs.size - 1]); - if (tail) - return arena.addTypePack(TypePack{std::move(head), follow(*tail)}); - else - return arena.addTypePack(TypePack{std::move(head), builtinTypes->errorRecoveryTypePack()}); -} - -bool AnyTypeSummary::isAnyCall(const Scope* scope, AstExpr* expr, const Module* module, NotNull builtinTypes) -{ - if (auto call = expr->as()) - { - TypePackId args = reconstructTypePack(call->args, module, builtinTypes); - if (containsAny(args)) - return true; - - TypeId func = lookupType(call->func, module, builtinTypes); - if (containsAny(func)) - return true; - } - return false; -} - -bool AnyTypeSummary::hasVariadicAnys(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull builtinTypes) -{ - if (expr->vararg && expr->varargAnnotation) - { - auto annot = lookupPackAnnotation(expr->varargAnnotation, module); - if (annot && containsAny(*annot)) - { - return true; - } - } - return false; -} - -bool AnyTypeSummary::hasArgAnys(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull builtinTypes) -{ - if (expr->args.size > 0) - { - for (const AstLocal* arg : expr->args) - { - if (arg->annotation) - { - auto annot = lookupAnnotation(arg->annotation, module, builtinTypes); - if (containsAny(annot)) - { - return true; - } - } - } - } - return false; -} - -bool AnyTypeSummary::hasAnyReturns(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull builtinTypes) -{ - if (!expr->returnAnnotation) - { - return false; - } - - for (AstType* ret : expr->returnAnnotation->types) - { - if (containsAny(lookupAnnotation(ret, module, builtinTypes))) - { - return true; - } - } - - if (expr->returnAnnotation->tailType) - { - auto annot = lookupPackAnnotation(expr->returnAnnotation->tailType, module); - if (annot && containsAny(*annot)) - { - return true; - } - } - - return false; -} - -bool AnyTypeSummary::isAnyCast(const Scope* scope, AstExpr* expr, const Module* module, NotNull builtinTypes) -{ - if (auto cast = expr->as()) - { - auto annot = lookupAnnotation(cast->annotation, module, builtinTypes); - if (containsAny(annot)) - { - return true; - } - } - return false; -} - -TypeId AnyTypeSummary::lookupAnnotation(AstType* annotation, const Module* module, NotNull builtintypes) -{ - if (FFlag::DebugLuauMagicTypes) - { - if (auto ref = annotation->as(); ref && ref->parameters.size > 0) - { - if (auto ann = ref->parameters.data[0].type) - { - TypeId argTy = lookupAnnotation(ref->parameters.data[0].type, module, builtintypes); - return follow(argTy); - } - } - } - - const TypeId* ty = module->astResolvedTypes.find(annotation); - if (ty) - return checkForTypeFunctionInhabitance(follow(*ty), annotation->location); - else - return checkForTypeFunctionInhabitance(builtintypes->errorRecoveryType(), annotation->location); -} - -TypeId AnyTypeSummary::checkForTypeFunctionInhabitance(const TypeId instance, const Location location) -{ - if (seenTypeFunctionInstances.find(instance)) - return instance; - seenTypeFunctionInstances.insert(instance); - - return instance; -} - -std::optional AnyTypeSummary::lookupPackAnnotation(AstTypePack* annotation, const Module* module) -{ - const TypePackId* tp = module->astResolvedTypePacks.find(annotation); - if (tp != nullptr) - return {follow(*tp)}; - return {}; -} - -bool AnyTypeSummary::containsAny(TypeId typ) -{ - typ = follow(typ); - - if (auto t = seen.find(typ); t && !*t) - { - return false; - } - - seen[typ] = false; - - RecursionCounter counter{&recursionCount}; - if (recursionCount >= FInt::LuauAnySummaryRecursionLimit) - { - return false; - } - - bool found = false; - - if (auto ty = get(typ)) - { - found = true; - } - else if (auto ty = get(typ)) - { - found = true; - } - else if (auto ty = get(typ)) - { - for (auto& [_name, prop] : ty->props) - { - if (FFlag::LuauSolverV2) - { - if (auto newT = follow(prop.readTy)) - { - if (containsAny(*newT)) - found = true; - } - else if (auto newT = follow(prop.writeTy)) - { - if (containsAny(*newT)) - found = true; - } - } - else - { - if (containsAny(prop.type())) - found = true; - } - } - } - else if (auto ty = get(typ)) - { - for (auto part : ty->parts) - { - if (containsAny(part)) - { - found = true; - } - } - } - else if (auto ty = get(typ)) - { - for (auto option : ty->options) - { - if (containsAny(option)) - { - found = true; - } - } - } - else if (auto ty = get(typ)) - { - if (containsAny(ty->argTypes)) - found = true; - else if (containsAny(ty->retTypes)) - found = true; - } - - seen[typ] = found; - - return found; -} - -bool AnyTypeSummary::containsAny(TypePackId typ) -{ - typ = follow(typ); - - if (auto t = seen.find(typ); t && !*t) - { - return false; - } - - seen[typ] = false; - - auto [head, tail] = flatten(typ); - bool found = false; - - for (auto tp : head) - { - if (containsAny(tp)) - found = true; - } - - if (tail) - { - if (auto vtp = get(tail)) - { - if (auto ty = get(follow(vtp->ty))) - { - found = true; - } - } - else if (auto tftp = get(tail)) - { - - for (TypePackId tp : tftp->packArguments) - { - if (containsAny(tp)) - { - found = true; - } - } - - for (TypeId t : tftp->typeArguments) - { - if (containsAny(t)) - { - found = true; - } - } - } - } - - seen[typ] = found; - - return found; -} - -const Scope* AnyTypeSummary::findInnerMostScope(const Location location, const Module* module) -{ - const Scope* bestScope = module->getModuleScope().get(); - - bool didNarrow = false; - do - { - didNarrow = false; - for (auto scope : bestScope->children) - { - if (scope->location.encloses(location)) - { - bestScope = scope.get(); - didNarrow = true; - break; - } - } - } while (didNarrow && bestScope->children.size() > 0); - - return bestScope; -} - -std::optional AnyTypeSummary::matchRequire(const AstExprCall& call) -{ - const char* require = "require"; - - if (call.args.size != 1) - return std::nullopt; - - const AstExprGlobal* funcAsGlobal = call.func->as(); - if (!funcAsGlobal || funcAsGlobal->name != require) - return std::nullopt; - - if (call.args.size != 1) - return std::nullopt; - - return call.args.data[0]; -} - -AstNode* AnyTypeSummary::getNode(AstStatBlock* root, AstNode* node) -{ - FindReturnAncestry finder(node, root->location.end); - root->visit(&finder); - - if (!finder.currNode) - finder.currNode = node; - - LUAU_ASSERT(finder.found && finder.currNode); - return finder.currNode; -} - -bool AnyTypeSummary::FindReturnAncestry::visit(AstStatLocalFunction* node) -{ - currNode = node; - return !found; -} - -bool AnyTypeSummary::FindReturnAncestry::visit(AstStatFunction* node) -{ - currNode = node; - return !found; -} - -bool AnyTypeSummary::FindReturnAncestry::visit(AstType* node) -{ - return !found; -} - -bool AnyTypeSummary::FindReturnAncestry::visit(AstNode* node) -{ - if (node == stat) - { - found = true; - } - - if (node->location.end == rootEnd && stat->location.end >= rootEnd) - { - currNode = node; - found = true; - } - - return !found; -} - - -AnyTypeSummary::TypeInfo::TypeInfo(Pattern code, std::string node, TelemetryTypePair type) - : code(code) - , node(node) - , type(type) -{ -} - -AnyTypeSummary::FindReturnAncestry::FindReturnAncestry(AstNode* stat, Position rootEnd) - : stat(stat) - , rootEnd(rootEnd) -{ -} - -AnyTypeSummary::AnyTypeSummary() {} - -} // namespace Luau \ No newline at end of file diff --git a/Analysis/src/AstJsonEncoder.cpp b/Analysis/src/AstJsonEncoder.cpp index 881bc85b..9318a8fa 100644 --- a/Analysis/src/AstJsonEncoder.cpp +++ b/Analysis/src/AstJsonEncoder.cpp @@ -1151,6 +1151,8 @@ struct AstJsonEncoder : public AstVisitor return writeString("checked"); case AstAttr::Type::Native: return writeString("native"); + case AstAttr::Type::Deprecated: + return writeString("deprecated"); } } diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 1972d559..0da7e56e 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -33,6 +33,7 @@ LUAU_FASTINT(LuauCheckRecursionLimit) LUAU_FASTFLAG(DebugLuauLogSolverToJson) LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) +LUAU_FASTFLAGVARIABLE(LuauPropagateExpectedTypesForCalls) LUAU_FASTFLAG(DebugLuauGreedyGeneralization) LUAU_FASTFLAGVARIABLE(LuauTrackInteriorFreeTypesOnScope) @@ -46,6 +47,8 @@ LUAU_FASTFLAGVARIABLE(LuauDoNotLeakNilInRefinement) LUAU_FASTFLAGVARIABLE(LuauExtraFollows) LUAU_FASTFLAG(LuauUserTypeFunTypecheck) +LUAU_FASTFLAG(LuauDeprecatedAttribute) + namespace Luau { @@ -547,7 +550,7 @@ void ConstraintGenerator::computeRefinement( refis->get(proposition->key->def)->shouldAppendNilType = (sense || !eq) && containsSubscriptedDefinition(proposition->key->def) && !proposition->implicitFromCall; } - else + else { refis->get(proposition->key->def)->shouldAppendNilType = (sense || !eq) && containsSubscriptedDefinition(proposition->key->def); } @@ -1357,6 +1360,23 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatRepeat* rep return ControlFlow::None; } +static void propagateDeprecatedAttributeToConstraint(ConstraintV& c, const AstExprFunction* func) +{ + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + if (GeneralizationConstraint* genConstraint = c.get_if()) + { + genConstraint->hasDeprecatedAttribute = func->hasAttribute(AstAttr::Type::Deprecated); + } +} + +static void propagateDeprecatedAttributeToType(TypeId signature, const AstExprFunction* func) +{ + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + FunctionType* fty = getMutable(signature); + LUAU_ASSERT(fty); + fty->isDeprecatedFunction = func->hasAttribute(AstAttr::Type::Deprecated); +} + ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFunction* function) { // Local @@ -1394,6 +1414,9 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti std::unique_ptr c = std::make_unique(constraintScope, function->name->location, GeneralizationConstraint{functionType, sig.signature}); + if (FFlag::LuauDeprecatedAttribute) + propagateDeprecatedAttributeToConstraint(c->c, function->func); + Constraint* previous = nullptr; forEachConstraint( start, @@ -1417,7 +1440,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti module->astTypes[function->func] = functionType; } else + { module->astTypes[function->func] = sig.signature; + if (FFlag::LuauDeprecatedAttribute) + propagateDeprecatedAttributeToType(sig.signature, function->func); + } return ControlFlow::None; } @@ -1458,7 +1485,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f TypeId generalizedType = arena->addType(BlockedType{}); if (sigFullyDefined) + { emplaceType(asMutable(generalizedType), sig.signature); + if (FFlag::LuauDeprecatedAttribute) + propagateDeprecatedAttributeToType(sig.signature, function->func); + } else { const ScopePtr& constraintScope = sig.signatureScope ? sig.signatureScope : sig.bodyScope; @@ -1466,6 +1497,9 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f NotNull c = addConstraint(constraintScope, function->name->location, GeneralizationConstraint{generalizedType, sig.signature}); getMutable(generalizedType)->setOwner(c); + if (FFlag::LuauDeprecatedAttribute) + propagateDeprecatedAttributeToConstraint(c->c, function->func); + Constraint* previous = nullptr; forEachConstraint( start, @@ -1981,6 +2015,8 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareFunc TypeId fnType = arena->addType(FunctionType{TypeLevel{}, funScope.get(), std::move(genericTys), std::move(genericTps), paramPack, retPack, defn}); FunctionType* ftv = getMutable(fnType); ftv->isCheckedFunction = global->isCheckedFunction(); + if (FFlag::LuauDeprecatedAttribute) + ftv->isDeprecatedFunction = global->hasAttribute(AstAttr::Type::Deprecated); ftv->argNames.reserve(global->paramNames.size); for (const auto& el : global->paramNames) @@ -2148,13 +2184,23 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* } else if (i < exprArgs.size() - 1 || !(arg->is() || arg->is())) { - auto [ty, refinement] = check(scope, arg, /*expectedType*/ std::nullopt, /*forceSingleton*/ false, /*generalize*/ false); + std::optional expectedType = std::nullopt; + if (FFlag::LuauPropagateExpectedTypesForCalls && i < expectedTypesForCall.size()) + { + expectedType = expectedTypesForCall[i]; + } + auto [ty, refinement] = check(scope, arg, expectedType, /*forceSingleton*/ false, /*generalize*/ false); args.push_back(ty); argumentRefinements.push_back(refinement); } else { - auto [tp, refis] = checkPack(scope, arg, {}); + std::vector> expectedTypes = {}; + if (FFlag::LuauPropagateExpectedTypesForCalls && i < expectedTypesForCall.size()) + { + expectedTypes.insert(expectedTypes.end(), expectedTypesForCall.begin() + int(i), expectedTypesForCall.end()); + } + auto [tp, refis] = checkPack(scope, arg, expectedTypes); argTail = tp; argumentRefinements.insert(argumentRefinements.end(), refis.begin(), refis.end()); } @@ -2414,8 +2460,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantBool* Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local) { const RefinementKey* key = dfg->getRefinementKey(local); - std::optional rvalueDef = dfg->getRValueDefForCompoundAssign(local); - LUAU_ASSERT(key || rvalueDef); + LUAU_ASSERT(key); std::optional maybeTy; @@ -2423,11 +2468,6 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local) if (key) maybeTy = lookup(scope, local->location, key->def); - // if the current def doesn't have a type, we might be doing a compound assignment - // and therefore might need to look at the rvalue def instead. - if (!maybeTy && rvalueDef) - maybeTy = lookup(scope, local->location, *rvalueDef); - if (maybeTy) { TypeId ty = follow(*maybeTy); @@ -2443,11 +2483,9 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local) Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* global) { const RefinementKey* key = dfg->getRefinementKey(global); - std::optional rvalueDef = dfg->getRValueDefForCompoundAssign(global); - LUAU_ASSERT(key || rvalueDef); + LUAU_ASSERT(key); - // we'll use whichever of the two definitions we have here. - DefId def = key ? key->def : *rvalueDef; + DefId def = key->def; /* prepopulateGlobalScope() has already added all global functions to the environment by this point, so any * global that is not already in-scope is definitely an unknown symbol. @@ -3572,6 +3610,8 @@ TypeId ConstraintGenerator::resolveFunctionType( // how to quantify/instantiate it. FunctionType ftv{TypeLevel{}, scope.get(), {}, {}, argTypes, returnTypes}; ftv.isCheckedFunction = fn->isCheckedFunction(); + if (FFlag::LuauDeprecatedAttribute) + ftv.isDeprecatedFunction = fn->hasAttribute(AstAttr::Type::Deprecated); // This replicates the behavior of the appropriate FunctionType // constructors. diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index dbe3c767..99ab1bc8 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -38,6 +38,9 @@ LUAU_FASTFLAGVARIABLE(LuauTrackInteriorFreeTablesOnScope) LUAU_FASTFLAGVARIABLE(LuauPrecalculateMutatedFreeTypes2) LUAU_FASTFLAGVARIABLE(DebugLuauGreedyGeneralization) LUAU_FASTFLAG(LuauSearchForRefineableType) +LUAU_FASTFLAG(LuauDeprecatedAttribute) +LUAU_FASTFLAG(LuauBidirectionalInferenceCollectIndexerTypes) +LUAU_FASTFLAG(LuauNewTypeFunReductionChecks2) namespace Luau { @@ -625,14 +628,6 @@ bool ConstraintSolver::isDone() const struct TypeSearcher : TypeVisitor { - enum struct Polarity: uint8_t - { - None = 0b00, - Positive = 0b01, - Negative = 0b10, - Mixed = 0b11, - }; - TypeId needle; Polarity current = Polarity::Positive; @@ -748,12 +743,12 @@ void ConstraintSolver::generalizeOneType(TypeId ty) switch (ts.result) { - case TypeSearcher::Polarity::None: + case Polarity::None: asMutable(ty)->reassign(Type{BoundType{upperBound}}); break; - case TypeSearcher::Polarity::Negative: - case TypeSearcher::Polarity::Mixed: + case Polarity::Negative: + case Polarity::Mixed: if (get(upperBound) && ts.count > 1) { asMutable(ty)->reassign(Type{GenericType{tyScope}}); @@ -763,15 +758,17 @@ void ConstraintSolver::generalizeOneType(TypeId ty) asMutable(ty)->reassign(Type{BoundType{upperBound}}); break; - case TypeSearcher::Polarity::Positive: - if (get(lowerBound) && ts.count > 1) - { - asMutable(ty)->reassign(Type{GenericType{tyScope}}); - function->generics.emplace_back(ty); - } - else - asMutable(ty)->reassign(Type{BoundType{lowerBound}}); - break; + case Polarity::Positive: + if (get(lowerBound) && ts.count > 1) + { + asMutable(ty)->reassign(Type{GenericType{tyScope}}); + function->generics.emplace_back(ty); + } + else + asMutable(ty)->reassign(Type{BoundType{lowerBound}}); + break; + default: + LUAU_ASSERT(!"Unreachable"); } } } @@ -918,6 +915,15 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull(follow(generalizedType))) + { + if (c.hasDeprecatedAttribute) + fty->isDeprecatedFunction = true; + } + } } else { @@ -931,12 +937,12 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNullscope->interiorFreeTypes) for (TypeId ty : *constraint->scope->interiorFreeTypes) // NOLINT(bugprone-unchecked-optional-access) - generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, ty, /* avoidSealingTables */ false); + generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, ty); } else { for (TypeId ty : c.interiorTypes) - generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, ty, /* avoidSealingTables */ false); + generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, ty); } @@ -1352,7 +1358,7 @@ void ConstraintSolver::fillInDiscriminantTypes(NotNull constra if (isBlocked(*ty)) // We bind any unused discriminants to the `*no-refine*` type indicating that it can be safely ignored. emplaceType(asMutable(follow(*ty)), builtinTypes->noRefineType); - + // We also need to unconditionally unblock these types, otherwise // you end up with funky looking "Blocked on *no-refine*." unblock(*ty, constraint->location); @@ -1558,6 +1564,43 @@ static AstExpr* unwrapGroup(AstExpr* expr) return expr; } +struct ContainsGenerics : public TypeOnceVisitor +{ + DenseHashSet generics{nullptr}; + + bool found = false; + + bool visit(TypeId ty) override + { + return !found; + } + + bool visit(TypeId ty, const GenericType&) override + { + found |= generics.contains(ty); + return true; + } + + bool visit(TypeId ty, const TypeFunctionInstanceType&) override + { + return !found; + } + + bool visit(TypePackId tp, const GenericTypePack&) override + { + found |= generics.contains(tp); + return !found; + } + + bool hasGeneric(TypeId ty) + { + traverse(ty); + auto ret = found; + found = false; + return ret; + } +}; + bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull constraint) { TypeId fn = follow(c.fn); @@ -1600,36 +1643,49 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull replacements{nullptr}; DenseHashMap replacementPacks{nullptr}; + ContainsGenerics containsGenerics; + for (auto generic : ftv->generics) + { replacements[generic] = builtinTypes->unknownType; + if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes) + containsGenerics.generics.insert(generic); + } for (auto genericPack : ftv->genericPacks) + { replacementPacks[genericPack] = builtinTypes->unknownTypePack; + if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes) + containsGenerics.generics.insert(genericPack); + } // If the type of the function has generics, we don't actually want to push any of the generics themselves // into the argument types as expected types because this creates an unnecessary loop. Instead, we want to // replace these types with `unknown` (and `...unknown`) to keep any structure but not create the cycle. - if (!replacements.empty() || !replacementPacks.empty()) + if (!FFlag::LuauBidirectionalInferenceCollectIndexerTypes) { - Replacer replacer{arena, std::move(replacements), std::move(replacementPacks)}; - - std::optional res = replacer.substitute(fn); - if (res) + if (!replacements.empty() || !replacementPacks.empty()) { - if (*res != fn) + Replacer replacer{arena, std::move(replacements), std::move(replacementPacks)}; + + std::optional res = replacer.substitute(fn); + if (res) { - FunctionType* ftvMut = getMutable(*res); - LUAU_ASSERT(ftvMut); - ftvMut->generics.clear(); - ftvMut->genericPacks.clear(); + if (*res != fn) + { + FunctionType* ftvMut = getMutable(*res); + LUAU_ASSERT(ftvMut); + ftvMut->generics.clear(); + ftvMut->genericPacks.clear(); + } + + fn = *res; + ftv = get(*res); + LUAU_ASSERT(ftv); + + // we've potentially copied type functions here, so we need to reproduce their reduce constraint. + reproduceConstraints(constraint->scope, constraint->location, replacer); } - - fn = *res; - ftv = get(*res); - LUAU_ASSERT(ftv); - - // we've potentially copied type functions here, so we need to reproduce their reduce constraint. - reproduceConstraints(constraint->scope, constraint->location, replacer); } } @@ -1648,6 +1704,10 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull(expectedArgTy); const FunctionType* lambdaTy = get(actualArgTy); const AstExprFunction* lambdaExpr = expr->as(); @@ -2359,11 +2419,18 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNulllocation); + if (FFlag::LuauNewTypeFunReductionChecks2) + { + for (TypeId ity : result.irreducibleTypes) + uninhabitedTypeFunctions.insert(ity); + } + bool reductionFinished = result.blockedTypes.empty() && result.blockedPacks.empty(); ty = follow(ty); + // If we couldn't reduce this type function, stick it in the set! - if (get(ty)) + if (get(ty) && (!FFlag::LuauNewTypeFunReductionChecks2 || !result.irreducibleTypes.find(ty))) typeFunctionsToFinalize[ty] = constraint; if (force || reductionFinished) @@ -3283,7 +3350,7 @@ void ConstraintSolver::shiftReferences(TypeId source, TypeId target) } } -std::optional ConstraintSolver::generalizeFreeType(NotNull scope, TypeId type, bool avoidSealingTables) +std::optional ConstraintSolver::generalizeFreeType(NotNull scope, TypeId type) { TypeId t = follow(type); if (get(t)) @@ -3298,7 +3365,7 @@ std::optional ConstraintSolver::generalizeFreeType(NotNull scope, // that until all constraint generation is complete. } - return generalize(NotNull{arena}, builtinTypes, scope, generalizedTypes, type, avoidSealingTables); + return generalize(NotNull{arena}, builtinTypes, scope, generalizedTypes, type); } bool ConstraintSolver::hasUnresolvedConstraints(TypeId ty) diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index cf393612..d8391ea5 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -82,12 +82,6 @@ std::optional DataFlowGraph::getDefOptional(const AstExpr* expr) const return NotNull{*def}; } -std::optional DataFlowGraph::getRValueDefForCompoundAssign(const AstExpr* expr) const -{ - auto def = compoundAssignDefs.find(expr); - return def ? std::optional(*def) : std::nullopt; -} - DefId DataFlowGraph::getDef(const AstLocal* local) const { auto def = localDefs.find(local); diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index d4693c9c..1f8ec09a 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -1,8 +1,6 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/BuiltinDefinitions.h" -LUAU_FASTFLAGVARIABLE(LuauDebugInfoDefn) - namespace Luau { @@ -215,15 +213,6 @@ declare debug: { )BUILTIN_SRC"; -static const std::string kBuiltinDefinitionDebugSrc_DEPRECATED = R"BUILTIN_SRC( - -declare debug: { - info: ((thread: thread, level: number, options: string) -> R...) & ((level: number, options: string) -> R...) & ((func: (A...) -> R1..., options: string) -> R2...), - traceback: ((message: string?, level: number?) -> string) & ((thread: thread, message: string?, level: number?) -> string), -} - -)BUILTIN_SRC"; - static const std::string kBuiltinDefinitionUtf8Src = R"BUILTIN_SRC( declare utf8: { @@ -309,7 +298,7 @@ std::string getBuiltinDefinitionSource() result += kBuiltinDefinitionOsSrc; result += kBuiltinDefinitionCoroutineSrc; result += kBuiltinDefinitionTableSrc; - result += FFlag::LuauDebugInfoDefn ? kBuiltinDefinitionDebugSrc : kBuiltinDefinitionDebugSrc_DEPRECATED; + result += kBuiltinDefinitionDebugSrc; result += kBuiltinDefinitionUtf8Src; result += kBuiltinDefinitionBufferSrc; result += kBuiltinDefinitionVectorSrc; diff --git a/Analysis/src/FragmentAutocomplete.cpp b/Analysis/src/FragmentAutocomplete.cpp index edcab3a5..76820e3d 100644 --- a/Analysis/src/FragmentAutocomplete.cpp +++ b/Analysis/src/FragmentAutocomplete.cpp @@ -28,10 +28,9 @@ LUAU_FASTINT(LuauTypeInferIterationLimit); LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTFLAG(LuauAutocompleteRefactorsForIncrementalAutocomplete) -LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteBugfixes) LUAU_FASTFLAGVARIABLE(LuauMixedModeDefFinderTraversesTypeOf) LUAU_FASTFLAGVARIABLE(LuauCloneIncrementalModule) -LUAU_FASTFLAGVARIABLE(LogFragmentsFromAutocomplete) +LUAU_FASTFLAGVARIABLE(DebugLogFragmentsFromAutocomplete) LUAU_FASTFLAGVARIABLE(LuauBetterCursorInCommentDetection) LUAU_FASTFLAGVARIABLE(LuauAllFreeTypesHaveScopes) LUAU_FASTFLAGVARIABLE(LuauFragmentAcSupportsReporter) @@ -42,6 +41,7 @@ LUAU_FASTFLAGVARIABLE(LuauCloneReturnTypePack) LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteDemandBasedCloning) LUAU_FASTFLAG(LuauUserTypeFunTypecheck) LUAU_FASTFLAGVARIABLE(LuauFragmentNoTypeFunEval) +LUAU_FASTFLAGVARIABLE(LuauBetterScopeSelection) namespace { @@ -87,6 +87,333 @@ void cloneModuleMap( } } +static std::pair getDocumentOffsets(std::string_view src, const Position& startPos, const Position& endPos); + +// when typing a function partially, get the span of the first line +// e.g. local function fn() : ... - typically we want to provide autocomplete results if you're +// editing type annotations in this range +Location getFunctionDeclarationExtents(AstExprFunction* exprFn, AstExpr* exprName = nullptr, AstLocal* localName = nullptr) +{ + auto fnBegin = exprFn->location.begin; + auto fnEnd = exprFn->location.end; + if (auto returnAnnot = exprFn->returnAnnotation) + { + if (returnAnnot->tailType) + fnEnd = returnAnnot->tailType->location.end; + else if (returnAnnot->types.size != 0) + fnEnd = returnAnnot->types.data[returnAnnot->types.size - 1]->location.end; + } + else if (exprFn->args.size != 0) + { + auto last = exprFn->args.data[exprFn->args.size - 1]; + if (last->annotation) + fnEnd = last->annotation->location.end; + else + fnEnd = last->location.end; + } + else if (exprFn->genericPacks.size != 0) + fnEnd = exprFn->genericPacks.data[exprFn->genericPacks.size - 1]->location.end; + else if (exprFn->generics.size != 0) + fnEnd = exprFn->generics.data[exprFn->generics.size - 1]->location.end; + else if (exprName) + fnEnd = exprName->location.end; + else if (localName) + fnEnd = localName->location.end; + return Location{fnBegin, fnEnd}; +}; + +Location getAstStatForExtents(AstStatFor* forStat) +{ + auto begin = forStat->location.begin; + auto end = forStat->location.end; + if (forStat->step) + end = forStat->step->location.end; + else if (forStat->to) + end = forStat->to->location.end; + else if (forStat->from) + end = forStat->from->location.end; + else if (forStat->var) + end = forStat->var->location.end; + + return Location{begin, end}; +} + +Location getFragmentLocation(AstStat* nearestStatement, const Position& cursorPosition) +{ + Location empty{cursorPosition, cursorPosition}; + if (nearestStatement) + { + Location nonEmpty{nearestStatement->location.begin, cursorPosition}; + // If your sibling is a do block, do nothing + if (auto doEnd = nearestStatement->as()) + return empty; + + // If you're inside the body of the function and this is your sibling, empty fragment + // If you're outside the body (e.g. you're typing stuff out, non-empty) + if (auto fn = nearestStatement->as()) + { + auto loc = getFunctionDeclarationExtents(fn->func, fn->name, /* local */ nullptr); + if (loc.containsClosed(cursorPosition)) + return nonEmpty; + else if (fn->func->body->location.containsClosed(cursorPosition) || fn->location.end <= cursorPosition) + return empty; + else if (fn->func->location.contains(cursorPosition)) + return nonEmpty; + } + + if (auto fn = nearestStatement->as()) + { + auto loc = getFunctionDeclarationExtents(fn->func, /* global func */ nullptr, fn->name); + if (loc.containsClosed(cursorPosition)) + return nonEmpty; + else if (fn->func->body->location.containsClosed(cursorPosition) || fn->location.end <= cursorPosition) + return empty; + else if (fn->func->location.contains(cursorPosition)) + return nonEmpty; + } + + if (auto wh = nearestStatement->as()) + { + if (!wh->hasDo) + return nonEmpty; + else + return empty; + } + + if (auto forStat = nearestStatement->as()) + { + if (!forStat->hasDo) + return nonEmpty; + else + return empty; + } + + if (auto forIn = nearestStatement->as()) + { + // If we don't have a do statement + if (!forIn->hasDo) + return nonEmpty; + else + return empty; + } + + if (auto ifS = nearestStatement->as()) + { + auto conditionExtents = Location{ifS->location.begin, ifS->condition->location.end}; + if (conditionExtents.containsClosed(cursorPosition)) + return nonEmpty; + else if (ifS->thenbody->location.containsClosed(cursorPosition)) + return empty; + else if (auto elseS = ifS->elsebody) + { + if (auto elseIf = ifS->elsebody->as()) + { + + if (elseIf->thenbody->hasEnd) + return empty; + else + return {elseS->location.begin, cursorPosition}; + } + return empty; + } + } + + return nonEmpty; + } + return empty; +} + +struct NearestStatementFinder : public AstVisitor +{ + explicit NearestStatementFinder(const Position& cursorPosition) + : cursor(cursorPosition) + { + } + + bool visit(AstStatBlock* block) override + { + if (block->location.containsClosed(cursor)) + { + parent = block; + for (auto v : block->body) + { + if (v->location.begin <= cursor) + { + nearest = v; + } + } + return true; + } + else + return false; + } + + const Position& cursor; + AstStat* nearest = nullptr; + AstStatBlock* parent = nullptr; +}; + +FragmentRegion getFragmentRegion(AstStatBlock* root, const Position& cursorPosition) +{ + NearestStatementFinder nsf{cursorPosition}; + root->visit(&nsf); + AstStatBlock* parent = root; + if (nsf.parent) + parent = nsf.parent; + return FragmentRegion{getFragmentLocation(nsf.nearest, cursorPosition), nsf.nearest, parent}; +}; + +FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* stale, const Position& cursorPos, AstStatBlock* lastGoodParse) +{ + // the freshest ast can sometimes be null if the parse was bad. + if (lastGoodParse == nullptr) + return {}; + FragmentRegion region = getFragmentRegion(lastGoodParse, cursorPos); + std::vector ancestry = findAncestryAtPositionForAutocomplete(stale, cursorPos); + LUAU_ASSERT(ancestry.size() >= 1); + // We should only pick up locals that are before the region + DenseHashMap localMap{AstName()}; + std::vector localStack; + + for (AstNode* node : ancestry) + { + if (auto block = node->as()) + { + for (auto stat : block->body) + { + if (stat->location.begin < region.fragmentLocation.begin) + { + // This statement precedes the current one + if (auto statLoc = stat->as()) + { + for (auto v : statLoc->vars) + { + localStack.push_back(v); + localMap[v->name] = v; + } + } + else if (auto locFun = stat->as()) + { + localStack.push_back(locFun->name); + localMap[locFun->name->name] = locFun->name; + if (locFun->location.contains(cursorPos)) + { + for (AstLocal* loc : locFun->func->args) + { + localStack.push_back(loc); + localMap[loc->name] = loc; + } + } + } + else if (auto globFun = stat->as()) + { + if (globFun->location.contains(cursorPos)) + { + for (AstLocal* loc : globFun->func->args) + { + localStack.push_back(loc); + localMap[loc->name] = loc; + } + } + } + else if (auto typeFun = stat->as(); typeFun) + { + if (typeFun->location.contains(cursorPos)) + { + for (AstLocal* loc : typeFun->body->args) + { + localStack.push_back(loc); + localMap[loc->name] = loc; + } + } + } + else if (auto forL = stat->as()) + { + if (forL->var && forL->var->location.begin < region.fragmentLocation.begin) + { + localStack.push_back(forL->var); + localMap[forL->var->name] = forL->var; + } + } + else if (auto forIn = stat->as()) + { + for (auto var : forIn->vars) + { + if (var->location.begin < region.fragmentLocation.begin) + { + localStack.push_back(var); + localMap[var->name] = var; + } + } + } + } + } + } + + if (auto exprFunc = node->as()) + { + if (exprFunc->location.contains(cursorPos)) + { + for (auto v : exprFunc->args) + { + localStack.push_back(v); + localMap[v->name] = v; + } + } + } + } + + return {localMap, localStack, ancestry, region.nearestStatement, region.parentBlock, region.fragmentLocation}; +} + +std::optional parseFragment( + AstStatBlock* stale, + AstStatBlock* mostRecentParse, + AstNameTable* names, + std::string_view src, + const Position& cursorPos, + std::optional fragmentEndPosition +) +{ + if (mostRecentParse == nullptr) + return std::nullopt; + FragmentAutocompleteAncestryResult result = findAncestryForFragmentParse(stale, cursorPos, mostRecentParse); + AstStat* nearestStatement = result.nearestStatement; + + Position startPos = result.fragmentSelectionRegion.begin; + Position endPos = fragmentEndPosition.value_or(result.fragmentSelectionRegion.end); + + auto [offsetStart, parseLength] = getDocumentOffsets(src, startPos, endPos); + const char* srcStart = src.data() + offsetStart; + std::string_view dbg = src.substr(offsetStart, parseLength); + FragmentParseResult fragmentResult; + fragmentResult.fragmentToParse = std::string(dbg); + // For the duration of the incremental parse, we want to allow the name table to re-use duplicate names + if (FFlag::DebugLogFragmentsFromAutocomplete) + logLuau("Fragment Selected", dbg); + + ParseOptions opts; + opts.allowDeclarationSyntax = false; + opts.captureComments = true; + opts.parseFragment = FragmentParseResumeSettings{std::move(result.localMap), std::move(result.localStack), startPos}; + ParseResult p = Luau::Parser::parse(srcStart, parseLength, *names, *fragmentResult.alloc, opts); + // This means we threw a ParseError and we should decline to offer autocomplete here. + if (p.root == nullptr) + return std::nullopt; + + std::vector fabricatedAncestry = std::move(result.ancestry); + std::vector fragmentAncestry = findAncestryAtPositionForAutocomplete(p.root, cursorPos); + fabricatedAncestry.insert(fabricatedAncestry.end(), fragmentAncestry.begin(), fragmentAncestry.end()); + if (nearestStatement == nullptr) + nearestStatement = p.root; + fragmentResult.root = p.root; + fragmentResult.ancestry = std::move(fabricatedAncestry); + fragmentResult.nearestStatement = nearestStatement; + fragmentResult.commentLocations = std::move(p.commentLocations); + fragmentResult.scopePos = result.parentBlock->location.begin; + return fragmentResult; +} + struct UsageFinder : public AstVisitor { @@ -158,6 +485,7 @@ void cloneTypesFromFragment( const ModulePtr& staleModule, NotNull destArena, NotNull dfg, + NotNull builtins, AstStatBlock* program, Scope* destScope ) @@ -188,6 +516,13 @@ void cloneTypesFromFragment( destScope->lvalueTypes[d] = Luau::cloneIncremental(pair->second.typeId, *destArena, cloneState, destScope); destScope->bindings[pair->first] = Luau::cloneIncremental(pair->second, *destArena, cloneState, destScope); } + else if (FFlag::LuauBetterScopeSelection) + { + destScope->lvalueTypes[d] = builtins->unknownType; + Binding b; + b.typeId = builtins->unknownType; + destScope->bindings[Symbol(loc)] = b; + } } // Second - any referenced type alias bindings need to be placed in scope so type annotation can be resolved. @@ -400,15 +735,10 @@ static FrontendModuleResolver& getModuleResolver(Frontend& frontend, std::option bool statIsBeforePos(const AstNode* stat, const Position& cursorPos) { - if (FFlag::LuauIncrementalAutocompleteBugfixes) - { - return (stat->location.begin < cursorPos); - } - - return stat->location.begin < cursorPos && stat->location.begin.line < cursorPos.line; + return (stat->location.begin < cursorPos); } -FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* root, const Position& cursorPos) +FragmentAutocompleteAncestryResult findAncestryForFragmentParse_DEPRECATED(AstStatBlock* root, const Position& cursorPos) { std::vector ancestry = findAncestryAtPositionForAutocomplete(root, cursorPos); // Should always contain the root AstStat @@ -437,7 +767,7 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro { for (auto stat : block->body) { - if (statIsBeforePos(stat, FFlag::LuauIncrementalAutocompleteBugfixes ? nearestStatement->location.begin : cursorPos)) + if (statIsBeforePos(stat, nearestStatement->location.begin)) { // This statement precedes the current one if (auto statLoc = stat->as()) @@ -486,17 +816,14 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro } } } - if (FFlag::LuauIncrementalAutocompleteBugfixes) + if (auto exprFunc = node->as()) { - if (auto exprFunc = node->as()) + if (exprFunc->location.contains(cursorPos)) { - if (exprFunc->location.contains(cursorPos)) + for (auto v : exprFunc->args) { - for (auto v : exprFunc->args) - { - localStack.push_back(v); - localMap[v->name] = v; - } + localStack.push_back(v); + localMap[v->name] = v; } } } @@ -513,7 +840,7 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro * Example - your document is "foo bar baz" and getDocumentOffsets is passed (0, 4), (0, 8). This function returns the pair {3, 5} * which corresponds to the string " bar " */ -std::pair getDocumentOffsets(const std::string_view& src, const Position& startPos, const Position& endPos) +static std::pair getDocumentOffsets(std::string_view src, const Position& startPos, const Position& endPos) { size_t lineCount = 0; size_t colCount = 0; @@ -570,14 +897,14 @@ std::pair getDocumentOffsets(const std::string_view& src, const return {min, len}; } -ScopePtr findClosestScope(const ModulePtr& module, const AstStat* nearestStatement) +ScopePtr findClosestScope_DEPRECATED(const ModulePtr& module, const AstStat* nearestStatement) { LUAU_ASSERT(module->hasModuleScope()); ScopePtr closest = module->getModuleScope(); // find the scope the nearest statement belonged to. - for (auto [loc, sc] : module->scopes) + for (const auto& [loc, sc] : module->scopes) { if (loc.encloses(nearestStatement->location) && closest->location.begin <= loc.begin) closest = sc; @@ -586,7 +913,23 @@ ScopePtr findClosestScope(const ModulePtr& module, const AstStat* nearestStateme return closest; } -std::optional parseFragment( +ScopePtr findClosestScope(const ModulePtr& module, const Position& scopePos) +{ + LUAU_ASSERT(module->hasModuleScope()); + + ScopePtr closest = module->getModuleScope(); + + // find the scope the nearest statement belonged to. + for (const auto& [loc, sc] : module->scopes) + { + if (sc->location.contains(scopePos) && closest->location.begin < sc->location.begin) + closest = sc; + } + + return closest; +} + +std::optional parseFragment_DEPRECATED( AstStatBlock* root, AstNameTable* names, std::string_view src, @@ -594,7 +937,7 @@ std::optional parseFragment( std::optional fragmentEndPosition ) { - FragmentAutocompleteAncestryResult result = findAncestryForFragmentParse(root, cursorPos); + FragmentAutocompleteAncestryResult result = findAncestryForFragmentParse_DEPRECATED(root, cursorPos); AstStat* nearestStatement = result.nearestStatement; const Location& rootSpan = root->location; @@ -625,8 +968,8 @@ std::optional parseFragment( FragmentParseResult fragmentResult; fragmentResult.fragmentToParse = std::string(dbg.data(), parseLength); // For the duration of the incremental parse, we want to allow the name table to re-use duplicate names - if (FFlag::LogFragmentsFromAutocomplete) - logLuau(dbg); + if (FFlag::DebugLogFragmentsFromAutocomplete) + logLuau("Fragment Selected", dbg); ParseOptions opts; opts.allowDeclarationSyntax = false; @@ -1025,7 +1368,14 @@ FragmentTypeCheckResult typecheckFragment_( reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeStart); cloneTypesFromFragment( - cloneState, closestScope.get(), stale, NotNull{&incrementalModule->internalTypes}, NotNull{&dfg}, root, freshChildOfNearestScope.get() + cloneState, + closestScope.get(), + stale, + NotNull{&incrementalModule->internalTypes}, + NotNull{&dfg}, + frontend.builtinTypes, + root, + freshChildOfNearestScope.get() ); reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeEnd); @@ -1086,6 +1436,7 @@ std::pair typecheckFragment( std::optional opts, std::string_view src, std::optional fragmentEndPosition, + AstStatBlock* recentParse, IFragmentAutocompleteReporter* reporter ) { @@ -1106,7 +1457,9 @@ std::pair typecheckFragment( std::optional tryParse; if (FFlag::LuauModuleHoldsAstRoot) { - tryParse = parseFragment(module->root, module->names.get(), src, cursorPos, fragmentEndPosition); + tryParse = FFlag::LuauBetterScopeSelection + ? parseFragment(module->root, recentParse, module->names.get(), src, cursorPos, fragmentEndPosition) + : parseFragment_DEPRECATED(module->root, module->names.get(), src, cursorPos, fragmentEndPosition); } else { @@ -1117,15 +1470,12 @@ std::pair typecheckFragment( return {}; } - if (FFlag::LuauIncrementalAutocompleteBugfixes) + if (sourceModule->allocator.get() != module->allocator.get()) { - if (sourceModule->allocator.get() != module->allocator.get()) - { - return {FragmentTypeCheckStatus::SkipAutocomplete, {}}; - } + return {FragmentTypeCheckStatus::SkipAutocomplete, {}}; } - tryParse = parseFragment(sourceModule->root, sourceModule->names.get(), src, cursorPos, fragmentEndPosition); + tryParse = parseFragment_DEPRECATED(sourceModule->root, sourceModule->names.get(), src, cursorPos, fragmentEndPosition); reportWaypoint(reporter, FragmentAutocompleteWaypoint::ParseFragmentEnd); } @@ -1138,7 +1488,8 @@ std::pair typecheckFragment( return {FragmentTypeCheckStatus::SkipAutocomplete, {}}; FrontendOptions frontendOptions = opts.value_or(frontend.options); - const ScopePtr& closestScope = findClosestScope(module, parseResult.nearestStatement); + const ScopePtr& closestScope = FFlag::LuauBetterScopeSelection ? findClosestScope(module, parseResult.scopePos) + : findClosestScope_DEPRECATED(module, parseResult.nearestStatement); FragmentTypeCheckResult result = FFlag::LuauIncrementalAutocompleteDemandBasedCloning ? typecheckFragment_(frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter) @@ -1174,14 +1525,15 @@ FragmentAutocompleteStatusResult tryFragmentAutocomplete( context.opts, std::move(stringCompletionCB), context.DEPRECATED_fragmentEndPosition, + context.freshParse.root, FFlag::LuauFragmentAcSupportsReporter ? context.reporter : nullptr ); return {FragmentAutocompleteStatus::Success, std::move(fragmentAutocomplete)}; } catch (const Luau::InternalCompilerError& e) { - if (FFlag::LogFragmentsFromAutocomplete) - logLuau(e.what()); + if (FFlag::DebugLogFragmentsFromAutocomplete) + logLuau("tryFragmentAutocomplete exception", e.what()); return {FragmentAutocompleteStatus::InternalIce, std::nullopt}; } } @@ -1194,6 +1546,7 @@ FragmentAutocompleteResult fragmentAutocomplete( std::optional opts, StringCompletionCallback callback, std::optional fragmentEndPosition, + AstStatBlock* recentParse, IFragmentAutocompleteReporter* reporter ) { @@ -1215,14 +1568,14 @@ FragmentAutocompleteResult fragmentAutocomplete( return {}; } - auto [tcStatus, tcResult] = typecheckFragment(frontend, moduleName, cursorPosition, opts, src, fragmentEndPosition, reporter); + auto [tcStatus, tcResult] = typecheckFragment(frontend, moduleName, cursorPosition, opts, src, fragmentEndPosition, recentParse, reporter); if (tcStatus == FragmentTypeCheckStatus::SkipAutocomplete) return {}; reportWaypoint(reporter, FragmentAutocompleteWaypoint::TypecheckFragmentEnd); auto globalScope = (opts && opts->forAutocomplete) ? frontend.globalsForAutocomplete.globalScope.get() : frontend.globals.globalScope.get(); - if (FFlag::LogFragmentsFromAutocomplete) - logLuau(src); + if (FFlag::DebugLogFragmentsFromAutocomplete) + logLuau("Fragment Autocomplete Source Script", src); TypeArena arenaForFragmentAutocomplete; auto result = Luau::autocomplete_( tcResult.incrementalModule, diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 8cbcc1b7..c2225e86 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -1,7 +1,6 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Frontend.h" -#include "Luau/AnyTypeSummary.h" #include "Luau/BuiltinDefinitions.h" #include "Luau/Clone.h" #include "Luau/Common.h" @@ -51,9 +50,8 @@ LUAU_FASTFLAGVARIABLE(LuauModuleHoldsAstRoot) LUAU_FASTFLAGVARIABLE(LuauFixMultithreadTypecheck) -LUAU_FASTFLAG(StudioReportLuauAny2) - LUAU_FASTFLAGVARIABLE(LuauSelectivelyRetainDFGArena) +LUAU_FASTFLAG(LuauTypeFunResultInAutocomplete) namespace Luau { @@ -461,20 +459,6 @@ CheckResult Frontend::check(const ModuleName& name, std::optionallintResult; - - if (FFlag::StudioReportLuauAny2 && item.options.retainFullTypeGraphs) - { - if (item.module) - { - const SourceModule& sourceModule = *item.sourceModule; - if (sourceModule.mode == Luau::Mode::Strict) - { - item.module->ats.root = toString(sourceModule.root); - } - item.module->ats.rootSrc = sourceModule.root; - item.module->ats.traverse(item.module.get(), sourceModule.root, NotNull{&builtinTypes_}); - } - } } return checkResult; @@ -1658,7 +1642,7 @@ ModulePtr check( SimplifierPtr simplifier = newSimplifier(NotNull{&result->internalTypes}, builtinTypes); TypeFunctionRuntime typeFunctionRuntime{iceHandler, NotNull{&limits}}; - typeFunctionRuntime.allowEvaluation = sourceModule.parseErrors.empty(); + typeFunctionRuntime.allowEvaluation = FFlag::LuauTypeFunResultInAutocomplete || sourceModule.parseErrors.empty(); ConstraintGenerator cg{ result, diff --git a/Analysis/src/Generalization.cpp b/Analysis/src/Generalization.cpp index e5f47a90..5b5361e8 100644 --- a/Analysis/src/Generalization.cpp +++ b/Analysis/src/Generalization.cpp @@ -30,7 +30,6 @@ struct MutatingGeneralizer : TypeOnceVisitor std::vector genericPacks; bool isWithinFunction = false; - bool avoidSealingTables = false; MutatingGeneralizer( NotNull arena, @@ -38,8 +37,7 @@ struct MutatingGeneralizer : TypeOnceVisitor NotNull scope, NotNull> cachedTypes, DenseHashMap positiveTypes, - DenseHashMap negativeTypes, - bool avoidSealingTables + DenseHashMap negativeTypes ) : TypeOnceVisitor(/* skipBoundTypes */ true) , arena(arena) @@ -48,7 +46,6 @@ struct MutatingGeneralizer : TypeOnceVisitor , cachedTypes(cachedTypes) , positiveTypes(std::move(positiveTypes)) , negativeTypes(std::move(negativeTypes)) - , avoidSealingTables(avoidSealingTables) { } @@ -145,7 +142,7 @@ struct MutatingGeneralizer : TypeOnceVisitor TypeId onlyType = it->parts[0]; LUAU_ASSERT(onlyType != needle); emplaceType(asMutable(needle), onlyType); - } + } else if (FFlag::LuauGeneralizationRemoveRecursiveUpperBound2 && it->parts.empty()) { emplaceType(asMutable(needle), builtinTypes->unknownType); @@ -292,8 +289,7 @@ struct MutatingGeneralizer : TypeOnceVisitor TableType* tt = getMutable(ty); LUAU_ASSERT(tt); - if (!avoidSealingTables) - tt->state = TableState::Sealed; + tt->state = TableState::Sealed; return true; } @@ -332,26 +328,19 @@ struct FreeTypeSearcher : TypeVisitor { } - enum Polarity - { - Positive, - Negative, - Both, - }; - - Polarity polarity = Positive; + Polarity polarity = Polarity::Positive; void flip() { switch (polarity) { - case Positive: - polarity = Negative; + case Polarity::Positive: + polarity = Polarity::Negative; break; - case Negative: - polarity = Positive; + case Polarity::Negative: + polarity = Polarity::Positive; break; - case Both: + default: break; } } @@ -363,7 +352,7 @@ struct FreeTypeSearcher : TypeVisitor { switch (polarity) { - case Positive: + case Polarity::Positive: { if (seenPositive.contains(ty)) return true; @@ -371,7 +360,7 @@ struct FreeTypeSearcher : TypeVisitor seenPositive.insert(ty); return false; } - case Negative: + case Polarity::Negative: { if (seenNegative.contains(ty)) return true; @@ -379,7 +368,7 @@ struct FreeTypeSearcher : TypeVisitor seenNegative.insert(ty); return false; } - case Both: + case Polarity::Mixed: { if (seenPositive.contains(ty) && seenNegative.contains(ty)) return true; @@ -388,6 +377,8 @@ struct FreeTypeSearcher : TypeVisitor seenNegative.insert(ty); return false; } + default: + LUAU_ASSERT(!"Unreachable"); } return false; @@ -418,16 +409,18 @@ struct FreeTypeSearcher : TypeVisitor switch (polarity) { - case Positive: + case Polarity::Positive: positiveTypes[ty]++; break; - case Negative: + case Polarity::Negative: negativeTypes[ty]++; break; - case Both: + case Polarity::Mixed: positiveTypes[ty]++; negativeTypes[ty]++; break; + default: + LUAU_ASSERT(!"Unreachable"); } return true; @@ -442,16 +435,18 @@ struct FreeTypeSearcher : TypeVisitor { switch (polarity) { - case Positive: + case Polarity::Positive: positiveTypes[ty]++; break; - case Negative: + case Polarity::Negative: negativeTypes[ty]++; break; - case Both: + case Polarity::Mixed: positiveTypes[ty]++; negativeTypes[ty]++; break; + default: + LUAU_ASSERT(!"Unreachable"); } } @@ -464,7 +459,7 @@ struct FreeTypeSearcher : TypeVisitor LUAU_ASSERT(prop.isShared() || FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete); Polarity p = polarity; - polarity = Both; + polarity = Polarity::Mixed; traverse(prop.type()); polarity = p; } @@ -508,16 +503,18 @@ struct FreeTypeSearcher : TypeVisitor switch (polarity) { - case Positive: + case Polarity::Positive: positiveTypes[tp]++; break; - case Negative: + case Polarity::Negative: negativeTypes[tp]++; break; - case Both: + case Polarity::Mixed: positiveTypes[tp]++; negativeTypes[tp]++; break; + default: + LUAU_ASSERT(!"Unreachable"); } return true; @@ -972,8 +969,7 @@ std::optional generalize( NotNull builtinTypes, NotNull scope, NotNull> cachedTypes, - TypeId ty, - bool avoidSealingTables + TypeId ty ) { ty = follow(ty); @@ -984,7 +980,7 @@ std::optional generalize( FreeTypeSearcher fts{scope, cachedTypes}; fts.traverse(ty); - MutatingGeneralizer gen{arena, builtinTypes, scope, cachedTypes, std::move(fts.positiveTypes), std::move(fts.negativeTypes), avoidSealingTables}; + MutatingGeneralizer gen{arena, builtinTypes, scope, cachedTypes, std::move(fts.positiveTypes), std::move(fts.negativeTypes)}; gen.traverse(ty); diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index a2bcb247..dc3bbb64 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -19,6 +19,8 @@ LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauAttribute) LUAU_FASTFLAGVARIABLE(LintRedundantNativeAttribute) +LUAU_FASTFLAG(LuauDeprecatedAttribute) + namespace Luau { @@ -2280,6 +2282,57 @@ private: { } + bool visit(AstExprLocal* node) override + { + if (FFlag::LuauDeprecatedAttribute) + { + + const FunctionType* fty = getFunctionType(node); + bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty); + + if (shouldReport) + report(node->location, node->local->name.value); + } + + return true; + } + + bool visit(AstExprGlobal* node) override + { + if (FFlag::LuauDeprecatedAttribute) + { + const FunctionType* fty = getFunctionType(node); + bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty); + + if (shouldReport) + report(node->location, node->name.value); + } + + return true; + } + + bool visit(AstStatLocalFunction* node) override + { + if (FFlag::LuauDeprecatedAttribute) + { + check(node->func); + return false; + } + else + return true; + } + + bool visit(AstStatFunction* node) override + { + if (FFlag::LuauDeprecatedAttribute) + { + check(node->func); + return false; + } + else + return true; + } + bool visit(AstExprIndexName* node) override { if (std::optional ty = context->getType(node->expr)) @@ -2325,18 +2378,59 @@ private: if (prop && prop->deprecated) report(node->location, *prop, cty->name.c_str(), node->index.value); + else if (FFlag::LuauDeprecatedAttribute && prop) + { + if (std::optional ty = prop->readTy) + { + const FunctionType* fty = get(follow(ty)); + bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty); + + if (shouldReport) + { + const char* className = nullptr; + if (AstExprGlobal* global = node->expr->as()) + className = global->name.value; + + const char* functionName = node->index.value; + + report(node->location, className, functionName); + } + } + } } else if (const TableType* tty = get(ty)) { auto prop = tty->props.find(node->index.value); - if (prop != tty->props.end() && prop->second.deprecated) + if (prop != tty->props.end()) { - // strip synthetic typeof() for builtin tables - if (tty->name && tty->name->compare(0, 7, "typeof(") == 0 && tty->name->back() == ')') - report(node->location, prop->second, tty->name->substr(7, tty->name->length() - 8).c_str(), node->index.value); - else - report(node->location, prop->second, tty->name ? tty->name->c_str() : nullptr, node->index.value); + if (prop->second.deprecated) + { + // strip synthetic typeof() for builtin tables + if (tty->name && tty->name->compare(0, 7, "typeof(") == 0 && tty->name->back() == ')') + report(node->location, prop->second, tty->name->substr(7, tty->name->length() - 8).c_str(), node->index.value); + else + report(node->location, prop->second, tty->name ? tty->name->c_str() : nullptr, node->index.value); + } + else if (FFlag::LuauDeprecatedAttribute) + { + if (std::optional ty = prop->second.readTy) + { + const FunctionType* fty = get(follow(ty)); + bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty); + + if (shouldReport) + { + const char* className = nullptr; + if (AstExprGlobal* global = node->expr->as()) + className = global->name.value; + + const char* functionName = node->index.value; + + report(node->location, className, functionName); + } + } + } } } } @@ -2355,6 +2449,26 @@ private: } } + void check(AstExprFunction* func) + { + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + LUAU_ASSERT(func); + + const FunctionType* fty = getFunctionType(func); + bool isDeprecated = fty && fty->isDeprecatedFunction; + + // If a function is deprecated, we don't want to flag its recursive uses. + // So we push it on a stack while its body is being analyzed. + // When a deprecated function is used, we check the stack to ensure that we are not inside that function. + if (isDeprecated) + pushScope(fty); + + func->visit(this); + + if (isDeprecated) + popScope(fty); + } + void report(const Location& location, const Property& prop, const char* container, const char* field) { std::string suggestion = prop.deprecatedSuggestion.empty() ? "" : format(", use '%s' instead", prop.deprecatedSuggestion.c_str()); @@ -2364,6 +2478,63 @@ private: else emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s' is deprecated%s", field, suggestion.c_str()); } + + void report(const Location& location, const char* tableName, const char* functionName) + { + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + + if (tableName) + emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s.%s' is deprecated", tableName, functionName); + else + emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s' is deprecated", functionName); + } + + void report(const Location& location, const char* functionName) + { + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + + emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Function '%s' is deprecated", functionName); + } + + std::vector functionTypeScopeStack; + + void pushScope(const FunctionType* fty) + { + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + LUAU_ASSERT(fty); + + functionTypeScopeStack.push_back(fty); + } + + void popScope(const FunctionType* fty) + { + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + LUAU_ASSERT(fty); + + LUAU_ASSERT(fty == functionTypeScopeStack.back()); + functionTypeScopeStack.pop_back(); + } + + bool inScope(const FunctionType* fty) const + { + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + LUAU_ASSERT(fty); + + return std::find(functionTypeScopeStack.begin(), functionTypeScopeStack.end(), fty) != functionTypeScopeStack.end(); + } + + const FunctionType* getFunctionType(AstExpr* node) + { + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + + std::optional ty = context->getType(node); + if (!ty) + return nullptr; + + const FunctionType* fty = get(follow(ty)); + + return fty; + } }; class LintTableOperations : AstVisitor diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index 1dbd6608..40ffc4a7 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -20,7 +20,7 @@ LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteCommentDetection) namespace Luau { -static void defaultLogLuau(std::string_view input) +static void defaultLogLuau(std::string_view context, std::string_view input) { // The default is to do nothing because we don't want to mess with // the xml parsing done by the dcr script. diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index eb86d401..de39e6fb 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -2,6 +2,7 @@ #include "Luau/NonStrictTypeChecker.h" #include "Luau/Ast.h" +#include "Luau/AstQuery.h" #include "Luau/Common.h" #include "Luau/Simplify.h" #include "Luau/Type.h" diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index f1ed03b4..24ed4d43 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -17,12 +17,15 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant) +LUAU_FASTFLAGVARIABLE(LuauNormalizeNegatedErrorToAnError) +LUAU_FASTFLAGVARIABLE(LuauNormalizeIntersectErrorToAnError) LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000) LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200) +LUAU_FASTINTVARIABLE(LuauNormalizeUnionLimit, 100) LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAGVARIABLE(LuauNormalizeNegationFix) LUAU_FASTFLAGVARIABLE(LuauFixInfiniteRecursionInNormalization) LUAU_FASTFLAGVARIABLE(LuauNormalizedBufferIsNotUnknown) +LUAU_FASTFLAGVARIABLE(LuauNormalizeLimitFunctionSet) namespace Luau { @@ -581,7 +584,7 @@ NormalizationResult Normalizer::isIntersectionInhabited(TypeId left, TypeId righ { left = follow(left); right = follow(right); - // We're asking if intersection is inahbited between left and right but we've already seen them .... + // We're asking if intersection is inhabited between left and right but we've already seen them .... if (cacheInhabitance) { @@ -1687,6 +1690,13 @@ NormalizationResult Normalizer::unionNormals(NormalizedType& here, const Normali return res; } + if (FFlag::LuauNormalizeLimitFunctionSet) + { + // Limit based on worst-case expansion of the function unions + if (here.functions.parts.size() * there.functions.parts.size() >= size_t(FInt::LuauNormalizeUnionLimit)) + return NormalizationResult::HitLimits; + } + here.booleans = unionOfBools(here.booleans, there.booleans); unionClasses(here.classes, there.classes); @@ -1698,6 +1708,7 @@ NormalizationResult Normalizer::unionNormals(NormalizedType& here, const Normali here.buffers = (get(there.buffers) ? here.buffers : there.buffers); unionFunctions(here.functions, there.functions); unionTables(here.tables, there.tables); + return NormalizationResult::True; } @@ -1737,7 +1748,7 @@ NormalizationResult Normalizer::intersectNormalWithNegationTy(TypeId toNegate, N return NormalizationResult::True; } -// See above for an explaination of `ignoreSmallerTyvars`. +// See above for an explanation of `ignoreSmallerTyvars`. NormalizationResult Normalizer::unionNormalWithTy( NormalizedType& here, TypeId there, @@ -3052,7 +3063,7 @@ NormalizationResult Normalizer::intersectTyvarsWithTy( return NormalizationResult::True; } -// See above for an explaination of `ignoreSmallerTyvars`. +// See above for an explanation of `ignoreSmallerTyvars`. NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars) { RecursionCounter _rc(&sharedState->counters.recursionCount); @@ -3070,11 +3081,17 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor return unionNormals(here, there, ignoreSmallerTyvars); } - // Limit based on worst-case expansion of the table intersection + // Limit based on worst-case expansion of the table/function intersections // This restriction can be relaxed when table intersection simplification is improved if (here.tables.size() * there.tables.size() >= size_t(FInt::LuauNormalizeIntersectionLimit)) return NormalizationResult::HitLimits; + if (FFlag::LuauNormalizeLimitFunctionSet) + { + if (here.functions.parts.size() * there.functions.parts.size() >= size_t(FInt::LuauNormalizeIntersectionLimit)) + return NormalizationResult::HitLimits; + } + here.booleans = intersectionOfBools(here.booleans, there.booleans); intersectClasses(here.classes, there.classes); @@ -3210,7 +3227,7 @@ NormalizationResult Normalizer::intersectNormalWithTy( { TypeId errors = here.errors; clearNormal(here); - here.errors = errors; + here.errors = FFlag::LuauNormalizeIntersectErrorToAnError && get(errors) ? errors : there; } else if (const PrimitiveType* ptv = get(there)) { @@ -3307,11 +3324,16 @@ NormalizationResult Normalizer::intersectNormalWithTy( clearNormal(here); return NormalizationResult::True; } + else if (FFlag::LuauNormalizeNegatedErrorToAnError && get(t)) + { + // ~error is still an error, so intersecting with the negation is the same as intersecting with a type + TypeId errors = here.errors; + clearNormal(here); + here.errors = FFlag::LuauNormalizeIntersectErrorToAnError && get(errors) ? errors : t; + } else if (auto nt = get(t)) { - if (FFlag::LuauNormalizeNegationFix) - here.tyvars = std::move(tyvars); - + here.tyvars = std::move(tyvars); return intersectNormalWithTy(here, nt->ty, seenTablePropPairs, seenSetTypes); } else diff --git a/Analysis/src/Substitution.cpp b/Analysis/src/Substitution.cpp index e00f0d3d..06903934 100644 --- a/Analysis/src/Substitution.cpp +++ b/Analysis/src/Substitution.cpp @@ -13,6 +13,7 @@ LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINTVARIABLE(LuauTarjanPreallocationSize, 256) LUAU_FASTFLAG(LuauSyntheticErrors) +LUAU_FASTFLAG(LuauDeprecatedAttribute) namespace Luau { @@ -102,6 +103,8 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a clone.tags = a.tags; clone.argNames = a.argNames; clone.isCheckedFunction = a.isCheckedFunction; + if (FFlag::LuauDeprecatedAttribute) + clone.isDeprecatedFunction = a.isDeprecatedFunction; return dest.addType(std::move(clone)); } else if constexpr (std::is_same_v) diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index e55255da..f0833e7a 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -22,6 +22,7 @@ #include LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity) +LUAU_FASTFLAGVARIABLE(LuauSubtypingStopAtNormFail) namespace Luau { @@ -415,6 +416,14 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy, NotNullnormalize(subTy), normalizer->normalize(superTy), scope); - if (semantic.isSubtype) + + if (FFlag::LuauSubtypingStopAtNormFail && semantic.normalizationTooComplex) + { + result = semantic; + } + else if (semantic.isSubtype) { semantic.reasoning.clear(); result = semantic; @@ -607,7 +621,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub if (!result.isSubtype && !result.normalizationTooComplex) { SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy), scope); - if (semantic.isSubtype) + + if (FFlag::LuauSubtypingStopAtNormFail && semantic.normalizationTooComplex) + { + result = semantic; + } + else if (semantic.isSubtype) { // Clear the semantic reasoning, as any reasonings within // potentially contain invalid paths. @@ -1082,6 +1101,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub for (TypeId ty : superUnion) { SubtypingResult next = isCovariantWith(env, subTy, ty, scope); + + if (FFlag::LuauSubtypingStopAtNormFail && next.normalizationTooComplex) + return SubtypingResult{false, /* normalizationTooComplex */ true}; + if (next.isSubtype) return SubtypingResult{true}; } @@ -1100,7 +1123,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Unio std::vector subtypings; size_t i = 0; for (TypeId ty : subUnion) + { subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Union})); + + if (FFlag::LuauSubtypingStopAtNormFail && subtypings.back().normalizationTooComplex) + return SubtypingResult{false, /* normalizationTooComplex */ true}; + } + return SubtypingResult::all(subtypings); } @@ -1110,7 +1139,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub std::vector subtypings; size_t i = 0; for (TypeId ty : superIntersection) + { subtypings.push_back(isCovariantWith(env, subTy, ty, scope).withSuperComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection})); + + if (FFlag::LuauSubtypingStopAtNormFail && subtypings.back().normalizationTooComplex) + return SubtypingResult{false, /* normalizationTooComplex */ true}; + } + return SubtypingResult::all(subtypings); } @@ -1120,7 +1155,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Inte std::vector subtypings; size_t i = 0; for (TypeId ty : subIntersection) + { subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection})); + + if (FFlag::LuauSubtypingStopAtNormFail && subtypings.back().normalizationTooComplex) + return SubtypingResult{false, /* normalizationTooComplex */ true}; + } + return SubtypingResult::any(subtypings); } @@ -1410,7 +1451,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Meta // of the supertype table. // // There's a flaw here in that if the __index metamethod contributes a new - // field that would satisfy the subtyping relationship, we'll erronously say + // field that would satisfy the subtyping relationship, we'll erroneously say // that the metatable isn't a subtype of the table, even though they have // compatible properties/shapes. We'll revisit this later when we have a // better understanding of how important this is. @@ -1760,7 +1801,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type { results.emplace_back(); for (TypeId superTy : superTypes) + { results.back().orElse(isCovariantWith(env, subTy, superTy, scope)); + + if (FFlag::LuauSubtypingStopAtNormFail && results.back().normalizationTooComplex) + return SubtypingResult{false, /* normalizationTooComplex */ true}; + } } return SubtypingResult::all(results); diff --git a/Analysis/src/TableLiteralInference.cpp b/Analysis/src/TableLiteralInference.cpp index f5127870..c1c0cd35 100644 --- a/Analysis/src/TableLiteralInference.cpp +++ b/Analysis/src/TableLiteralInference.cpp @@ -14,6 +14,8 @@ #include "Luau/Unifier2.h" LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceUpcast) +LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceCollectIndexerTypes) +LUAU_FASTFLAGVARIABLE(LuauBidirectionalFailsafe) namespace Luau { @@ -136,14 +138,13 @@ TypeId matchLiteralType( * things like replace explicit named properties with indexers as required * by the expected type. */ + if (!isLiteral(expr)) { if (FFlag::LuauBidirectionalInferenceUpcast) { auto result = subtyping->isSubtype(/*subTy=*/exprType, /*superTy=*/expectedType, unifier->scope); - return result.isSubtype - ? expectedType - : exprType; + return result.isSubtype ? expectedType : exprType; } else return exprType; @@ -152,11 +153,23 @@ TypeId matchLiteralType( expectedType = follow(expectedType); exprType = follow(exprType); - if (get(expectedType) || get(expectedType)) + if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes) { - // "Narrowing" to unknown or any is not going to do anything useful. - return exprType; + // The intent of `matchLiteralType` is to upcast values when it's safe + // to do so. it's always safe to upcast to `any` or `unknown`, so we + // can unconditionally do so here. + if (is(expectedType)) + return expectedType; } + else + { + if (get(expectedType) || get(expectedType)) + { + // "Narrowing" to unknown or any is not going to do anything useful. + return exprType; + } + } + if (expr->is()) { @@ -238,6 +251,15 @@ TypeId matchLiteralType( if (auto exprTable = expr->as()) { TableType* const tableTy = getMutable(exprType); + + // This can occur if we have an expression like: + // + // { x = {}, x = 42 } + // + // The type of this will be `{ x: number }` + if (FFlag::LuauBidirectionalFailsafe && !tableTy) + return exprType; + LUAU_ASSERT(tableTy); const TableType* expectedTableTy = get(expectedType); @@ -264,6 +286,9 @@ TypeId matchLiteralType( DenseHashSet keysToDelete{nullptr}; + DenseHashSet indexerKeyTypes{nullptr}; + DenseHashSet indexerValueTypes{nullptr}; + for (const AstExprTable::Item& item : exprTable->items) { if (isRecord(item)) @@ -271,6 +296,11 @@ TypeId matchLiteralType( const AstArray& s = item.key->as()->value; std::string keyStr{s.data, s.data + s.size}; auto it = tableTy->props.find(keyStr); + + // This can occur, potentially, if we are re-entrant. + if (FFlag::LuauBidirectionalFailsafe && it == tableTy->props.end()) + continue; + LUAU_ASSERT(it != tableTy->props.end()); Property& prop = it->second; @@ -307,10 +337,18 @@ TypeId matchLiteralType( toBlock ); - if (tableTy->indexer) - unifier->unify(matchedType, tableTy->indexer->indexResultType); + if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes) + { + indexerKeyTypes.insert(arena->addType(SingletonType{StringSingleton{keyStr}})); + indexerValueTypes.insert(matchedType); + } else - tableTy->indexer = TableIndexer{expectedTableTy->indexer->indexType, matchedType}; + { + if (tableTy->indexer) + unifier->unify(matchedType, tableTy->indexer->indexResultType); + else + tableTy->indexer = TableIndexer{expectedTableTy->indexer->indexType, matchedType}; + } keysToDelete.insert(item.key->as()); @@ -368,6 +406,11 @@ TypeId matchLiteralType( LUAU_ASSERT(matchedType); (*astExpectedTypes)[item.value] = matchedType; + // NOTE: We do *not* add to the potential indexer types here. + // I think this is correct to support something like: + // + // { [string]: number, foo: boolean } + // } else if (item.kind == AstExprTable::Item::List) { @@ -392,9 +435,18 @@ TypeId matchLiteralType( toBlock ); - // if the index result type is the prop type, we can replace it with the matched type here. - if (tableTy->indexer->indexResultType == *propTy) - tableTy->indexer->indexResultType = matchedType; + if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes) + { + indexerKeyTypes.insert(builtinTypes->numberType); + indexerValueTypes.insert(matchedType); + } + else + { + // if the index result type is the prop type, we can replace it with the matched type here. + if (tableTy->indexer->indexResultType == *propTy) + tableTy->indexer->indexResultType = matchedType; + } + } } else if (item.kind == AstExprTable::Item::General) @@ -416,6 +468,13 @@ TypeId matchLiteralType( // Populate expected types for non-string keys declared with [] (the code below will handle the case where they are strings) if (!item.key->as() && expectedTableTy->indexer) (*astExpectedTypes)[item.key] = expectedTableTy->indexer->indexType; + + if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes) + { + indexerKeyTypes.insert(tKey); + indexerValueTypes.insert(tProp); + } + } else LUAU_ASSERT(!"Unexpected"); @@ -477,9 +536,39 @@ TypeId matchLiteralType( // have one too. // TODO: If the expected table also has an indexer, we might want to // push the expected indexer's types into it. - if (expectedTableTy->indexer && !tableTy->indexer) + if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes && expectedTableTy->indexer) { - tableTy->indexer = expectedTableTy->indexer; + if (indexerValueTypes.size() > 0 && indexerKeyTypes.size() > 0) + { + TypeId inferredKeyType = builtinTypes->neverType; + TypeId inferredValueType = builtinTypes->neverType; + for (auto kt: indexerKeyTypes) + { + auto simplified = simplifyUnion(builtinTypes, arena, inferredKeyType, kt); + inferredKeyType = simplified.result; + } + for (auto vt: indexerValueTypes) + { + auto simplified = simplifyUnion(builtinTypes, arena, inferredValueType, vt); + inferredValueType = simplified.result; + } + tableTy->indexer = TableIndexer{inferredKeyType, inferredValueType}; + auto keyCheck = subtyping->isSubtype(inferredKeyType, expectedTableTy->indexer->indexType, unifier->scope); + if (keyCheck.isSubtype) + tableTy->indexer->indexType = expectedTableTy->indexer->indexType; + auto valueCheck = subtyping->isSubtype(inferredValueType, expectedTableTy->indexer->indexResultType, unifier->scope); + if (valueCheck.isSubtype) + tableTy->indexer->indexResultType = expectedTableTy->indexer->indexResultType; + } + else + LUAU_ASSERT(indexerKeyTypes.empty() && indexerValueTypes.empty()); + } + else + { + if (expectedTableTy->indexer && !tableTy->indexer) + { + tableTy->indexer = expectedTableTy->indexer; + } } } diff --git a/Analysis/src/Transpiler.cpp b/Analysis/src/Transpiler.cpp index f8ba378e..3d7ec6c2 100644 --- a/Analysis/src/Transpiler.cpp +++ b/Analysis/src/Transpiler.cpp @@ -10,11 +10,12 @@ #include #include -LUAU_FASTFLAG(LuauStoreCSTData) +LUAU_FASTFLAG(LuauStoreCSTData2) LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) LUAU_FASTFLAG(LuauAstTypeGroup3) LUAU_FASTFLAG(LuauFixDoBlockEndLocation) -LUAU_FASTFLAG(LuauParseOptionalAsNode) +LUAU_FASTFLAG(LuauParseOptionalAsNode2) +LUAU_FASTFLAG(LuauFixFunctionWithAttributesStartLocation) namespace { @@ -167,7 +168,7 @@ struct StringWriter : Writer void symbol(std::string_view s) override { - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { write(s); } @@ -257,7 +258,7 @@ public: first = !first; else { - if (FFlag::LuauStoreCSTData && commaPosition) + if (FFlag::LuauStoreCSTData2 && commaPosition) { writer.advance(*commaPosition); commaPosition++; @@ -1229,9 +1230,18 @@ struct Printer_DEPRECATED AstType* l = a->types.data[0]; AstType* r = a->types.data[1]; - auto lta = l->as(); - if (lta && lta->name == "nil") - std::swap(l, r); + if (FFlag::LuauParseOptionalAsNode2) + { + auto lta = l->as(); + if (lta && lta->name == "nil" && !r->is()) + std::swap(l, r); + } + else + { + auto lta = l->as(); + if (lta && lta->name == "nil") + std::swap(l, r); + } // it's still possible that we had a (T | U) or (T | nil) and not (nil | T) auto rta = r->as(); @@ -1254,7 +1264,7 @@ struct Printer_DEPRECATED for (size_t i = 0; i < a->types.size; ++i) { - if (FFlag::LuauParseOptionalAsNode) + if (FFlag::LuauParseOptionalAsNode2) { if (a->types.data[i]->is()) { @@ -1489,7 +1499,8 @@ struct Printer void visualize(AstExpr& expr) { - advance(expr.location.begin); + if (!expr.is() || FFlag::LuauFixFunctionWithAttributesStartLocation) + advance(expr.location.begin); if (const auto& a = expr.as()) { @@ -1623,6 +1634,17 @@ struct Printer } else if (const auto& a = expr.as()) { + for (const auto& attribute : a->attributes) + visualizeAttribute(*attribute); + if (FFlag::LuauFixFunctionWithAttributesStartLocation) + { + if (const auto cstNode = lookupCstNode(a)) + advance(cstNode->functionKeywordPosition); + } + else + { + advance(a->location.begin); + } writer.keyword("function"); visualizeFunctionBody(*a); } @@ -1874,7 +1896,8 @@ struct Printer void visualize(AstStat& program) { - advance(program.location.begin); + if ((!program.is() && !program.is()) || FFlag::LuauFixFunctionWithAttributesStartLocation) + advance(program.location.begin); if (const auto& block = program.as()) { @@ -2111,13 +2134,36 @@ struct Printer } else if (const auto& a = program.as()) { + for (const auto& attribute : a->func->attributes) + visualizeAttribute(*attribute); + if (FFlag::LuauFixFunctionWithAttributesStartLocation) + { + if (const auto cstNode = lookupCstNode(a)) + advance(cstNode->functionKeywordPosition); + } + else + { + advance(a->location.begin); + } writer.keyword("function"); visualize(*a->name); visualizeFunctionBody(*a->func); } else if (const auto& a = program.as()) { + for (const auto& attribute : a->func->attributes) + visualizeAttribute(*attribute); + const auto cstNode = lookupCstNode(a); + if (FFlag::LuauFixFunctionWithAttributesStartLocation) + { + if (cstNode) + advance(cstNode->localKeywordPosition); + } + else + { + advance(a->location.begin); + } writer.keyword("local"); @@ -2261,7 +2307,7 @@ struct Printer if (program.hasSemicolon) { - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) advanceBefore(program.location.end, 1); writer.symbol(";"); } @@ -2271,7 +2317,7 @@ struct Printer { const auto cstNode = lookupCstNode(&func); - // TODO(CLI-139347): need to handle attributes, argument types, and return type (incl. parentheses of return type) + // TODO(CLI-139347): need to handle return type (incl. parentheses of return type) if (func.generics.size > 0 || func.genericPacks.size > 0) { @@ -2427,6 +2473,23 @@ struct Printer } } + void visualizeAttribute(AstAttr& attribute) + { + advance(attribute.location.begin); + switch (attribute.type) + { + case AstAttr::Checked: + writer.keyword("@checked"); + break; + case AstAttr::Native: + writer.keyword("@native"); + break; + case AstAttr::Deprecated: + writer.keyword("@deprecated"); + break; + } + } + void visualizeTypeAnnotation(AstType& typeAnnotation) { advance(typeAnnotation.location.begin); @@ -2671,14 +2734,25 @@ struct Printer } else if (const auto& a = typeAnnotation.as()) { - if (a->types.size == 2) + const auto cstNode = lookupCstNode(a); + + if (!cstNode && a->types.size == 2) { AstType* l = a->types.data[0]; AstType* r = a->types.data[1]; - auto lta = l->as(); - if (lta && lta->name == "nil") - std::swap(l, r); + if (FFlag::LuauParseOptionalAsNode2) + { + auto lta = l->as(); + if (lta && lta->name == "nil" && !r->is()) + std::swap(l, r); + } + else + { + auto lta = l->as(); + if (lta && lta->name == "nil") + std::swap(l, r); + } // it's still possible that we had a (T | U) or (T | nil) and not (nil | T) auto rta = r->as(); @@ -2699,12 +2773,20 @@ struct Printer } } + if (cstNode && cstNode->leadingPosition) + { + advance(*cstNode->leadingPosition); + writer.symbol("|"); + } + + size_t separatorIndex = 0; for (size_t i = 0; i < a->types.size; ++i) { - if (FFlag::LuauParseOptionalAsNode) + if (FFlag::LuauParseOptionalAsNode2) { - if (a->types.data[i]->is()) + if (const auto optional = a->types.data[i]->as()) { + advance(optional->location.begin); writer.symbol("?"); continue; } @@ -2712,11 +2794,18 @@ struct Printer if (i > 0) { - writer.maybeSpace(a->types.data[i]->location.begin, 2); + if (cstNode && FFlag::LuauParseOptionalAsNode2) + { + // separatorIndex is only valid if `?` is handled as an AstTypeOptional + advance(cstNode->separatorPositions.data[separatorIndex]); + separatorIndex++; + } + else + writer.maybeSpace(a->types.data[i]->location.begin, 2); writer.symbol("|"); } - bool wrap = a->types.data[i]->as() || a->types.data[i]->as(); + bool wrap = !cstNode && (a->types.data[i]->as() || a->types.data[i]->as()); if (wrap) writer.symbol("("); @@ -2729,15 +2818,27 @@ struct Printer } else if (const auto& a = typeAnnotation.as()) { + const auto cstNode = lookupCstNode(a); + + // If the sizes are equal, we know there is a leading & token + if (cstNode && cstNode->leadingPosition) + { + advance(*cstNode->leadingPosition); + writer.symbol("&"); + } + for (size_t i = 0; i < a->types.size; ++i) { if (i > 0) { - writer.maybeSpace(a->types.data[i]->location.begin, 2); + if (cstNode) + advance(cstNode->separatorPositions.data[i - 1]); + else + writer.maybeSpace(a->types.data[i]->location.begin, 2); writer.symbol("&"); } - bool wrap = a->types.data[i]->as() || a->types.data[i]->as(); + bool wrap = !cstNode && (a->types.data[i]->as() || a->types.data[i]->as()); if (wrap) writer.symbol("("); @@ -2786,7 +2887,7 @@ std::string toString(AstNode* node) StringWriter writer; writer.pos = node->location.begin; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { Printer printer(writer, CstNodeMap{nullptr}); printer.writeTypes = true; @@ -2822,7 +2923,7 @@ void dump(AstNode* node) std::string transpile(AstStatBlock& block, const CstNodeMap& cstNodeMap) { StringWriter writer; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { Printer(writer, cstNodeMap).visualizeBlock(block); } @@ -2836,7 +2937,7 @@ std::string transpile(AstStatBlock& block, const CstNodeMap& cstNodeMap) std::string transpileWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap) { StringWriter writer; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { Printer printer(writer, cstNodeMap); printer.writeTypes = true; diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index 95ce6e6b..cd5fc050 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -13,7 +13,7 @@ #include -LUAU_FASTFLAG(LuauStoreCSTData) +LUAU_FASTFLAG(LuauStoreCSTData2) static char* allocateString(Luau::Allocator& allocator, std::string_view contents) { @@ -308,7 +308,7 @@ public: if (el) new (arg) - std::optional(AstArgumentName(AstName(el->name.c_str()), FFlag::LuauStoreCSTData ? Location() : el->location)); + std::optional(AstArgumentName(AstName(el->name.c_str()), FFlag::LuauStoreCSTData2 ? Location() : el->location)); else new (arg) std::optional(); } diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 41ace420..8df11f5e 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -33,6 +33,7 @@ LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) LUAU_FASTFLAGVARIABLE(LuauImproveTypePathsInErrors) LUAU_FASTFLAG(LuauUserTypeFunTypecheck) +LUAU_FASTFLAGVARIABLE(LuauTypeCheckerAcceptNumberConcats) namespace Luau { @@ -2229,10 +2230,21 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey) return builtinTypes->numberType; case AstExprBinary::Op::Concat: - testIsSubtype(leftType, builtinTypes->stringType, expr->left->location); - testIsSubtype(rightType, builtinTypes->stringType, expr->right->location); + { + if (FFlag::LuauTypeCheckerAcceptNumberConcats) + { + const TypeId numberOrString = module->internalTypes.addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}}); + testIsSubtype(leftType, numberOrString, expr->left->location); + testIsSubtype(rightType, numberOrString, expr->right->location); + } + else + { + testIsSubtype(leftType, builtinTypes->stringType, expr->left->location); + testIsSubtype(rightType, builtinTypes->stringType, expr->right->location); + } return builtinTypes->stringType; + } case AstExprBinary::Op::CompareGe: case AstExprBinary::Op::CompareGt: case AstExprBinary::Op::CompareLe: diff --git a/Analysis/src/TypeFunction.cpp b/Analysis/src/TypeFunction.cpp index a3735e59..3688ca33 100644 --- a/Analysis/src/TypeFunction.cpp +++ b/Analysis/src/TypeFunction.cpp @@ -48,22 +48,28 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyUseGuesserDepth, -1); LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies) LUAU_FASTFLAG(DebugLuauEqSatSimplification) +LUAU_FASTFLAG(LuauTypeFunResultInAutocomplete) LUAU_FASTFLAGVARIABLE(LuauMetatableTypeFunctions) LUAU_FASTFLAGVARIABLE(LuauClipNestedAndRecursiveUnion) LUAU_FASTFLAGVARIABLE(LuauIndexTypeFunctionImprovements) LUAU_FASTFLAGVARIABLE(LuauIndexTypeFunctionFunctionMetamethods) LUAU_FASTFLAGVARIABLE(LuauIntersectNotNil) LUAU_FASTFLAGVARIABLE(LuauSkipNoRefineDuringRefinement) +LUAU_FASTFLAGVARIABLE(LuauMetatablesHaveLength) LUAU_FASTFLAGVARIABLE(LuauDontForgetToReduceUnionFunc) LUAU_FASTFLAGVARIABLE(LuauSearchForRefineableType) LUAU_FASTFLAGVARIABLE(LuauIndexAnyIsAny) +LUAU_FASTFLAGVARIABLE(LuauSimplyRefineNotNil) +LUAU_FASTFLAGVARIABLE(LuauIndexDeferPendingIndexee) +LUAU_FASTFLAGVARIABLE(LuauNewTypeFunReductionChecks2) +LUAU_FASTFLAGVARIABLE(LuauReduceUnionFollowUnionType) namespace Luau { using TypeOrTypePackIdSet = DenseHashSet; -struct InstanceCollector : TypeOnceVisitor +struct InstanceCollector_DEPRECATED : TypeOnceVisitor { VecDeque tys; VecDeque tps; @@ -118,6 +124,153 @@ struct InstanceCollector : TypeOnceVisitor } }; +struct InstanceCollector : TypeOnceVisitor +{ + DenseHashSet recordedTys{nullptr}; + VecDeque tys; + DenseHashSet recordedTps{nullptr}; + VecDeque tps; + TypeOrTypePackIdSet shouldGuess{nullptr}; + std::vector typeFunctionInstanceStack; + std::vector cyclicInstance; + + bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override + { + // TypeVisitor performs a depth-first traversal in the absence of + // cycles. This means that by pushing to the front of the queue, we will + // try to reduce deeper instances first if we start with the first thing + // in the queue. Consider Add, number>, number>: + // we want to reduce the innermost Add instantiation + // first. + + typeFunctionInstanceStack.push_back(ty); + + if (DFInt::LuauTypeFamilyUseGuesserDepth >= 0 && int(typeFunctionInstanceStack.size()) > DFInt::LuauTypeFamilyUseGuesserDepth) + shouldGuess.insert(ty); + + if (!recordedTys.contains(ty)) + { + recordedTys.insert(ty); + tys.push_front(ty); + } + + for (TypeId p : tfit.typeArguments) + traverse(p); + + for (TypePackId p : tfit.packArguments) + traverse(p); + + typeFunctionInstanceStack.pop_back(); + + return false; + } + + void cycle(TypeId ty) override + { + TypeId t = follow(ty); + + if (get(t)) + { + // If we see a type a second time and it's in the type function stack, it's a real cycle + if (std::find(typeFunctionInstanceStack.begin(), typeFunctionInstanceStack.end(), t) != typeFunctionInstanceStack.end()) + cyclicInstance.push_back(t); + } + } + + bool visit(TypeId ty, const ClassType&) override + { + return false; + } + + bool visit(TypePackId tp, const TypeFunctionInstanceTypePack& tfitp) override + { + // TypeVisitor performs a depth-first traversal in the absence of + // cycles. This means that by pushing to the front of the queue, we will + // try to reduce deeper instances first if we start with the first thing + // in the queue. Consider Add, number>, number>: + // we want to reduce the innermost Add instantiation + // first. + + typeFunctionInstanceStack.push_back(tp); + + if (DFInt::LuauTypeFamilyUseGuesserDepth >= 0 && int(typeFunctionInstanceStack.size()) > DFInt::LuauTypeFamilyUseGuesserDepth) + shouldGuess.insert(tp); + + if (!recordedTps.contains(tp)) + { + recordedTps.insert(tp); + tps.push_front(tp); + } + + for (TypeId p : tfitp.typeArguments) + traverse(p); + + for (TypePackId p : tfitp.packArguments) + traverse(p); + + typeFunctionInstanceStack.pop_back(); + + return false; + } +}; + +struct UnscopedGenericFinder : TypeOnceVisitor +{ + std::vector scopeGenTys; + std::vector scopeGenTps; + bool foundUnscoped = false; + + bool visit(TypeId ty) override + { + // Once we have found an unscoped generic, we will stop the traversal + return !foundUnscoped; + } + + bool visit(TypePackId tp) override + { + // Once we have found an unscoped generic, we will stop the traversal + return !foundUnscoped; + } + + bool visit(TypeId ty, const GenericType&) override + { + if (std::find(scopeGenTys.begin(), scopeGenTys.end(), ty) == scopeGenTys.end()) + foundUnscoped = true; + + return false; + } + + bool visit(TypePackId tp, const GenericTypePack&) override + { + if (std::find(scopeGenTps.begin(), scopeGenTps.end(), tp) == scopeGenTps.end()) + foundUnscoped = true; + + return false; + } + + bool visit(TypeId ty, const FunctionType& ftv) override + { + size_t startTyCount = scopeGenTys.size(); + size_t startTpCount = scopeGenTps.size(); + + scopeGenTys.insert(scopeGenTys.end(), ftv.generics.begin(), ftv.generics.end()); + scopeGenTps.insert(scopeGenTps.end(), ftv.genericPacks.begin(), ftv.genericPacks.end()); + + traverse(ftv.argTypes); + traverse(ftv.retTypes); + + scopeGenTys.resize(startTyCount); + scopeGenTps.resize(startTpCount); + + return false; + } + + bool visit(TypeId ty, const ClassType&) override + { + return false; + } +}; + struct TypeFunctionReducer { TypeFunctionContext ctx; @@ -358,7 +511,6 @@ struct TypeFunctionReducer return false; } - void stepType() { TypeId subject = follow(queuedTys.front()); @@ -372,6 +524,26 @@ struct TypeFunctionReducer if (const TypeFunctionInstanceType* tfit = get(subject)) { + if (FFlag::LuauNewTypeFunReductionChecks2 && tfit->function->name == "user") + { + UnscopedGenericFinder finder; + finder.traverse(subject); + + if (finder.foundUnscoped) + { + // Do not step into this type again + irreducible.insert(subject); + + // Let the caller know this type will not become reducible + result.irreducibleTypes.insert(subject); + + if (FFlag::DebugLuauLogTypeFamilies) + printf("Irreducible due to an unscoped generic type\n"); + + return; + } + } + SkipTestResult testCyclic = testForSkippability(subject); if (!testParameters(subject, tfit) && testCyclic != SkipTestResult::CyclicTypeFunction) @@ -480,56 +652,114 @@ static FunctionGraphReductionResult reduceFunctionsInternal( FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, TypeFunctionContext ctx, bool force) { - InstanceCollector collector; - - try + if (FFlag::LuauNewTypeFunReductionChecks2) { - collector.traverse(entrypoint); + InstanceCollector collector; + + try + { + collector.traverse(entrypoint); + } + catch (RecursionLimitException&) + { + return FunctionGraphReductionResult{}; + } + + if (collector.tys.empty() && collector.tps.empty()) + return {}; + + return reduceFunctionsInternal( + std::move(collector.tys), + std::move(collector.tps), + std::move(collector.shouldGuess), + std::move(collector.cyclicInstance), + location, + ctx, + force + ); } - catch (RecursionLimitException&) + else { - return FunctionGraphReductionResult{}; + InstanceCollector_DEPRECATED collector; + + try + { + collector.traverse(entrypoint); + } + catch (RecursionLimitException&) + { + return FunctionGraphReductionResult{}; + } + + if (collector.tys.empty() && collector.tps.empty()) + return {}; + + return reduceFunctionsInternal( + std::move(collector.tys), + std::move(collector.tps), + std::move(collector.shouldGuess), + std::move(collector.cyclicInstance), + location, + ctx, + force + ); } - - if (collector.tys.empty() && collector.tps.empty()) - return {}; - - return reduceFunctionsInternal( - std::move(collector.tys), - std::move(collector.tps), - std::move(collector.shouldGuess), - std::move(collector.cyclicInstance), - location, - ctx, - force - ); } FunctionGraphReductionResult reduceTypeFunctions(TypePackId entrypoint, Location location, TypeFunctionContext ctx, bool force) { - InstanceCollector collector; - - try + if (FFlag::LuauNewTypeFunReductionChecks2) { - collector.traverse(entrypoint); + InstanceCollector collector; + + try + { + collector.traverse(entrypoint); + } + catch (RecursionLimitException&) + { + return FunctionGraphReductionResult{}; + } + + if (collector.tys.empty() && collector.tps.empty()) + return {}; + + return reduceFunctionsInternal( + std::move(collector.tys), + std::move(collector.tps), + std::move(collector.shouldGuess), + std::move(collector.cyclicInstance), + location, + ctx, + force + ); } - catch (RecursionLimitException&) + else { - return FunctionGraphReductionResult{}; + InstanceCollector_DEPRECATED collector; + + try + { + collector.traverse(entrypoint); + } + catch (RecursionLimitException&) + { + return FunctionGraphReductionResult{}; + } + + if (collector.tys.empty() && collector.tps.empty()) + return {}; + + return reduceFunctionsInternal( + std::move(collector.tys), + std::move(collector.tps), + std::move(collector.shouldGuess), + std::move(collector.cyclicInstance), + location, + ctx, + force + ); } - - if (collector.tys.empty() && collector.tps.empty()) - return {}; - - return reduceFunctionsInternal( - std::move(collector.tys), - std::move(collector.tps), - std::move(collector.shouldGuess), - std::move(collector.cyclicInstance), - location, - ctx, - force - ); } bool isPending(TypeId ty, ConstraintSolver* solver) @@ -624,6 +854,42 @@ static std::optional> tryDistributeTypeFunct return std::nullopt; } +struct FindUserTypeFunctionBlockers : TypeOnceVisitor +{ + NotNull ctx; + DenseHashSet blockingTypeMap{nullptr}; + std::vector blockingTypes; + + explicit FindUserTypeFunctionBlockers(NotNull ctx) + : TypeOnceVisitor(/* skipBoundTypes */ true) + , ctx(ctx) + { + } + + bool visit(TypeId ty) override + { + if (isPending(ty, ctx->solver)) + { + if (!blockingTypeMap.contains(ty)) + { + blockingTypeMap.insert(ty); + blockingTypes.push_back(ty); + } + } + return true; + } + + bool visit(TypePackId tp) override + { + return true; + } + + bool visit(TypeId ty, const ClassType&) override + { + return false; + } +}; + TypeFunctionReductionResult userDefinedTypeFunction( TypeId instance, const std::vector& typeParams, @@ -646,21 +912,38 @@ TypeFunctionReductionResult userDefinedTypeFunction( } // If type functions cannot be evaluated because of errors in the code, we do not generate any additional ones - if (!ctx->typeFunctionRuntime->allowEvaluation) + if (!ctx->typeFunctionRuntime->allowEvaluation || (FFlag::LuauTypeFunResultInAutocomplete && typeFunction->userFuncData.definition->hasErrors)) return {ctx->builtins->errorRecoveryType(), Reduction::MaybeOk, {}, {}}; - for (auto typeParam : typeParams) + if (FFlag::LuauNewTypeFunReductionChecks2) { - TypeId ty = follow(typeParam); + FindUserTypeFunctionBlockers check{ctx}; - // block if we need to - if (isPending(ty, ctx->solver)) - return {std::nullopt, Reduction::MaybeOk, {ty}, {}}; + for (auto typeParam : typeParams) + check.traverse(follow(typeParam)); + + if (!check.blockingTypes.empty()) + return {std::nullopt, Reduction::MaybeOk, check.blockingTypes, {}}; + } + else + { + for (auto typeParam : typeParams) + { + TypeId ty = follow(typeParam); + + // block if we need to + if (isPending(ty, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {ty}, {}}; + } } // Ensure that whole type function environment is registered for (auto& [name, definition] : typeFunction->userFuncData.environment) { + // Cannot evaluate if a potential dependency couldn't be parsed + if (FFlag::LuauTypeFunResultInAutocomplete && definition.first->hasErrors) + return {ctx->builtins->errorRecoveryType(), Reduction::MaybeOk, {}, {}}; + if (std::optional error = ctx->typeFunctionRuntime->registerFunction(definition.first)) { // Failure to register at this point means that original definition had to error out and should not have been present in the @@ -874,7 +1157,16 @@ TypeFunctionReductionResult lenTypeFunction( std::optional mmType = findMetatableEntry(ctx->builtins, dummy, operandTy, "__len", Location{}); if (!mmType) + { + if (FFlag::LuauMetatablesHaveLength) + { + // If we have a metatable type with no __len, this means we still have a table with default length function + if (get(normalizedOperand)) + return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}}; + } + return {std::nullopt, Reduction::Erroneous, {}, {}}; + } mmType = follow(*mmType); if (isPending(*mmType, ctx->solver)) @@ -1004,6 +1296,10 @@ std::optional TypeFunctionRuntime::registerFunction(AstStatTypeFunc if (!allowEvaluation) return std::nullopt; + // Do not evaluate type functions with parse errors inside + if (FFlag::LuauTypeFunResultInAutocomplete && function->hasErrors) + return std::nullopt; + prepareState(); lua_State* global = state.get(); @@ -1046,7 +1342,6 @@ std::optional TypeFunctionRuntime::registerFunction(AstStatTypeFunc std::string bytecode = builder.getBytecode(); - // Separate sandboxed thread for individual execution and private globals lua_State* L = lua_newthread(global); LuauTempThreadPopper popper(global); @@ -1923,6 +2218,18 @@ TypeFunctionReductionResult refineTypeFunction( } } + if (FFlag::LuauSimplyRefineNotNil) + { + if (auto negation = get(discriminant)) + { + if (auto primitive = get(follow(negation->ty)); primitive && primitive->type == PrimitiveType::NilType) + { + SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, target, discriminant); + return {result.result, {}}; + } + } + } + // If the target type is a table, then simplification already implements the logic to deal with refinements properly since the // type of the discriminant is guaranteed to only ever be an (arbitrarily-nested) table of a single property type. if (get(target)) @@ -2030,6 +2337,29 @@ struct CollectUnionTypeOptions : TypeOnceVisitor return false; } + bool visit(TypeId ty, const UnionType& ut) override + { + if (FFlag::LuauReduceUnionFollowUnionType) + { + // If we have something like: + // + // union + // + // We probably just want to consider this to be the same as + // + // union + return true; + } + else + { + // Copy of the default visit method. + options.insert(ty); + if (isPending(ty, ctx->solver)) + blockingTypes.insert(ty); + return false; + } + } + bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override { if (tfit.function->name != builtinTypeFunctions().unionFunc.name) @@ -2618,6 +2948,10 @@ TypeFunctionReductionResult indexFunctionImpl( ) { TypeId indexeeTy = follow(typeParams.at(0)); + + if (FFlag::LuauIndexDeferPendingIndexee && isPending(indexeeTy, ctx->solver)) + return {std::nullopt, Reduction::MaybeOk, {indexeeTy}, {}}; + std::shared_ptr indexeeNormTy = ctx->normalizer->normalize(indexeeTy); // if the indexee failed to normalize, we can't reduce, but know nothing about inhabitance. diff --git a/Analysis/src/TypeFunctionRuntime.cpp b/Analysis/src/TypeFunctionRuntime.cpp index b4c1f915..3627b90e 100644 --- a/Analysis/src/TypeFunctionRuntime.cpp +++ b/Analysis/src/TypeFunctionRuntime.cpp @@ -14,7 +14,6 @@ #include LUAU_DYNAMIC_FASTINT(LuauTypeFunctionSerdeIterationLimit) -LUAU_FASTFLAGVARIABLE(LuauTypeFunPrintFix) LUAU_FASTFLAGVARIABLE(LuauTypeFunReadWriteParents) namespace Luau @@ -1656,10 +1655,7 @@ static int print(lua_State* L) const char* s = luaL_tolstring(L, i, &l); // convert to string using __tostring et al if (i > 1) { - if (FFlag::LuauTypeFunPrintFix) - result.append(1, '\t'); - else - result.append('\t', 1); + result.append(1, '\t'); } result.append(s, l); lua_pop(L, 1); diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index 18c570be..e5af8a6a 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -650,27 +650,22 @@ struct FreeTypeSearcher : TypeVisitor { } - enum Polarity - { - Positive, - Negative, - Both, - }; - - Polarity polarity = Positive; + Polarity polarity = Polarity::Positive; void flip() { switch (polarity) { - case Positive: - polarity = Negative; + case Polarity::Positive: + polarity = Polarity::Negative; break; - case Negative: - polarity = Positive; + case Polarity::Negative: + polarity = Polarity::Positive; break; - case Both: + case Polarity::Mixed: break; + default: + LUAU_ASSERT(!"Unreachable"); } } @@ -681,7 +676,7 @@ struct FreeTypeSearcher : TypeVisitor { switch (polarity) { - case Positive: + case Polarity::Positive: { if (seenPositive.contains(ty)) return true; @@ -689,7 +684,7 @@ struct FreeTypeSearcher : TypeVisitor seenPositive.insert(ty); return false; } - case Negative: + case Polarity::Negative: { if (seenNegative.contains(ty)) return true; @@ -697,7 +692,7 @@ struct FreeTypeSearcher : TypeVisitor seenNegative.insert(ty); return false; } - case Both: + case Polarity::Mixed: { if (seenPositive.contains(ty) && seenNegative.contains(ty)) return true; @@ -706,6 +701,8 @@ struct FreeTypeSearcher : TypeVisitor seenNegative.insert(ty); return false; } + default: + LUAU_ASSERT(!"Unreachable"); } return false; @@ -736,16 +733,18 @@ struct FreeTypeSearcher : TypeVisitor switch (polarity) { - case Positive: + case Polarity::Positive: positiveTypes[ty]++; break; - case Negative: + case Polarity::Negative: negativeTypes[ty]++; break; - case Both: + case Polarity::Mixed: positiveTypes[ty]++; negativeTypes[ty]++; break; + default: + LUAU_ASSERT(!"Unreachable"); } return true; @@ -760,16 +759,18 @@ struct FreeTypeSearcher : TypeVisitor { switch (polarity) { - case Positive: + case Polarity::Positive: positiveTypes[ty]++; break; - case Negative: + case Polarity::Negative: negativeTypes[ty]++; break; - case Both: + case Polarity::Mixed: positiveTypes[ty]++; negativeTypes[ty]++; break; + default: + LUAU_ASSERT(!"Unreachable"); } } @@ -782,7 +783,7 @@ struct FreeTypeSearcher : TypeVisitor LUAU_ASSERT(prop.isShared()); Polarity p = polarity; - polarity = Both; + polarity = Polarity::Mixed; traverse(prop.type()); polarity = p; } @@ -826,16 +827,18 @@ struct FreeTypeSearcher : TypeVisitor switch (polarity) { - case Positive: + case Polarity::Positive: positiveTypes[tp]++; break; - case Negative: + case Polarity::Negative: negativeTypes[tp]++; break; - case Both: + case Polarity::Mixed: positiveTypes[tp]++; negativeTypes[tp]++; break; + default: + LUAU_ASSERT(!"Unreachable"); } return true; diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index 4d4e1280..e8649755 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -194,6 +194,7 @@ public: { Checked, Native, + Deprecated, }; AstAttr(const Location& location, Type type); @@ -453,6 +454,7 @@ public: void visit(AstVisitor* visitor) override; bool hasNativeAttribute() const; + bool hasAttribute(AstAttr::Type attributeType) const; AstArray attributes; AstArray generics; @@ -890,14 +892,22 @@ class AstStatTypeFunction : public AstStat public: LUAU_RTTI(AstStatTypeFunction); - AstStatTypeFunction(const Location& location, const AstName& name, const Location& nameLocation, AstExprFunction* body, bool exported); + AstStatTypeFunction( + const Location& location, + const AstName& name, + const Location& nameLocation, + AstExprFunction* body, + bool exported, + bool hasErrors + ); void visit(AstVisitor* visitor) override; AstName name; Location nameLocation; - AstExprFunction* body; - bool exported; + AstExprFunction* body = nullptr; + bool exported = false; + bool hasErrors = false; }; class AstStatDeclareGlobal : public AstStat @@ -950,6 +960,7 @@ public: void visit(AstVisitor* visitor) override; bool isCheckedFunction() const; + bool hasAttribute(AstAttr::Type attributeType) const; AstArray attributes; AstName name; @@ -1106,6 +1117,7 @@ public: void visit(AstVisitor* visitor) override; bool isCheckedFunction() const; + bool hasAttribute(AstAttr::Type attributeType) const; AstArray attributes; AstArray generics; @@ -1459,6 +1471,10 @@ public: { return visit(static_cast(node)); } + virtual bool visit(class AstStatTypeFunction* node) + { + return visit(static_cast(node)); + } virtual bool visit(class AstStatDeclareFunction* node) { return visit(static_cast(node)); diff --git a/Ast/include/Luau/Cst.h b/Ast/include/Luau/Cst.h index 8c6cf34c..c6489d9f 100644 --- a/Ast/include/Luau/Cst.h +++ b/Ast/include/Luau/Cst.h @@ -112,6 +112,7 @@ public: CstExprFunction(); + Position functionKeywordPosition{0, 0}; Position openGenericsPosition{0,0}; AstArray genericsCommaPositions; Position closeGenericsPosition{0,0}; @@ -274,13 +275,24 @@ public: Position opPosition; }; +class CstStatFunction : public CstNode +{ +public: + LUAU_CST_RTTI(CstStatFunction) + + explicit CstStatFunction(Position functionKeywordPosition); + + Position functionKeywordPosition; +}; + class CstStatLocalFunction : public CstNode { public: LUAU_CST_RTTI(CstStatLocalFunction) - explicit CstStatLocalFunction(Position functionKeywordPosition); + explicit CstStatLocalFunction(Position localKeywordPosition, Position functionKeywordPosition); + Position localKeywordPosition; Position functionKeywordPosition; }; @@ -421,6 +433,28 @@ public: Position closePosition; }; +class CstTypeUnion : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeUnion) + + CstTypeUnion(std::optional leadingPosition, AstArray separatorPositions); + + std::optional leadingPosition; + AstArray separatorPositions; +}; + +class CstTypeIntersection : public CstNode +{ +public: + LUAU_CST_RTTI(CstTypeIntersection) + + explicit CstTypeIntersection(std::optional leadingPosition, AstArray separatorPositions); + + std::optional leadingPosition; + AstArray separatorPositions; +}; + class CstTypeSingletonString : public CstNode { public: diff --git a/Ast/include/Luau/Parser.h b/Ast/include/Luau/Parser.h index 22137832..312f5a87 100644 --- a/Ast/include/Luau/Parser.h +++ b/Ast/include/Luau/Parser.h @@ -125,7 +125,7 @@ private: AstStat* parseFor(); // funcname ::= Name {`.' Name} [`:' Name] - AstExpr* parseFunctionName(Location start_DEPRECATED, bool& hasself, AstName& debugname); + AstExpr* parseFunctionName(bool& hasself, AstName& debugname); // function funcname funcbody LUAU_FORCEINLINE AstStat* parseFunctionStat(const AstArray& attributes = {nullptr, 0}); @@ -157,7 +157,9 @@ private: // type function Name ... end AstStat* parseTypeFunction(const Location& start, bool exported, Position typeKeywordPosition); - AstDeclaredClassProp parseDeclaredClassMethod(); + AstDeclaredClassProp parseDeclaredClassMethod(const AstArray& attributes); + AstDeclaredClassProp parseDeclaredClassMethod_DEPRECATED(); + // `declare global' Name: Type | // `declare function' Name`(' [parlist] `)' [`:` Type] @@ -228,9 +230,9 @@ private: Position colonPosition; }; - TableIndexerResult parseTableIndexer(AstTableAccess access, std::optional accessLocation); - // Remove with FFlagLuauStoreCSTData - AstTableIndexer* parseTableIndexer_DEPRECATED(AstTableAccess access, std::optional accessLocation); + TableIndexerResult parseTableIndexer(AstTableAccess access, std::optional accessLocation, Lexeme begin); + // Remove with FFlagLuauStoreCSTData2 + AstTableIndexer* parseTableIndexer_DEPRECATED(AstTableAccess access, std::optional accessLocation, Lexeme begin); AstTypeOrPack parseFunctionType(bool allowPack, const AstArray& attributes); AstType* parseFunctionTypeTail( diff --git a/Ast/src/Ast.cpp b/Ast/src/Ast.cpp index dd0779bb..37287162 100644 --- a/Ast/src/Ast.cpp +++ b/Ast/src/Ast.cpp @@ -3,9 +3,24 @@ #include "Luau/Common.h" +LUAU_FASTFLAG(LuauDeprecatedAttribute); + namespace Luau { +static bool hasAttributeInArray(const AstArray attributes, AstAttr::Type attributeType) +{ + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + + for (const auto attribute : attributes) + { + if (attribute->type == attributeType) + return true; + } + + return false; +} + static void visitTypeList(AstVisitor* visitor, const AstTypeList& list) { for (AstType* ty : list.types) @@ -277,6 +292,13 @@ bool AstExprFunction::hasNativeAttribute() const return false; } +bool AstExprFunction::hasAttribute(const AstAttr::Type attributeType) const +{ + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + + return hasAttributeInArray(attributes, attributeType); +} + AstExprTable::AstExprTable(const Location& location, const AstArray& items) : AstExpr(ClassIndex(), location) , items(items) @@ -791,13 +813,15 @@ AstStatTypeFunction::AstStatTypeFunction( const AstName& name, const Location& nameLocation, AstExprFunction* body, - bool exported + bool exported, + bool hasErrors ) : AstStat(ClassIndex(), location) , name(name) , nameLocation(nameLocation) , body(body) , exported(exported) + , hasErrors(hasErrors) { } @@ -894,6 +918,13 @@ bool AstStatDeclareFunction::isCheckedFunction() const return false; } +bool AstStatDeclareFunction::hasAttribute(AstAttr::Type attributeType) const +{ + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + + return hasAttributeInArray(attributes, attributeType); +} + AstStatDeclareClass::AstStatDeclareClass( const Location& location, const AstName& name, @@ -1057,6 +1088,13 @@ bool AstTypeFunction::isCheckedFunction() const return false; } +bool AstTypeFunction::hasAttribute(AstAttr::Type attributeType) const +{ + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + + return hasAttributeInArray(attributes, attributeType); +} + AstTypeTypeof::AstTypeTypeof(const Location& location, AstExpr* expr) : AstType(ClassIndex(), location) , expr(expr) diff --git a/Ast/src/Cst.cpp b/Ast/src/Cst.cpp index 7ed73c5f..3873a106 100644 --- a/Ast/src/Cst.cpp +++ b/Ast/src/Cst.cpp @@ -129,12 +129,19 @@ CstStatCompoundAssign::CstStatCompoundAssign(Position opPosition) { } -CstStatLocalFunction::CstStatLocalFunction(Position functionKeywordPosition) +CstStatFunction::CstStatFunction(Position functionKeywordPosition) : CstNode(CstClassIndex()) , functionKeywordPosition(functionKeywordPosition) { } +CstStatLocalFunction::CstStatLocalFunction(Position localKeywordPosition, Position functionKeywordPosition) + : CstNode(CstClassIndex()) + , localKeywordPosition(localKeywordPosition) + , functionKeywordPosition(functionKeywordPosition) +{ +} + CstGenericType::CstGenericType(std::optional defaultEqualsPosition) : CstNode(CstClassIndex()) , defaultEqualsPosition(defaultEqualsPosition) @@ -221,6 +228,20 @@ CstTypeTypeof::CstTypeTypeof(Position openPosition, Position closePosition) { } +CstTypeUnion::CstTypeUnion(std::optional leadingPosition, AstArray separatorPositions) + : CstNode(CstClassIndex()) + , leadingPosition(leadingPosition) + , separatorPositions(separatorPositions) +{ +} + +CstTypeIntersection::CstTypeIntersection(std::optional leadingPosition, AstArray separatorPositions) + : CstNode(CstClassIndex()) + , leadingPosition(leadingPosition) + , separatorPositions(separatorPositions) +{ +} + CstTypeSingletonString::CstTypeSingletonString(AstArray sourceString, CstExprConstantString::QuoteStyle quoteStyle, unsigned int blockDepth) : CstNode(CstClassIndex()) , sourceString(sourceString) diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 474efd6a..283baf52 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -20,14 +20,21 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTFLAGVARIABLE(LuauSolverV2) LUAU_FASTFLAGVARIABLE(LuauAllowComplexTypesInGenericParams) LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryForTableTypes) -LUAU_FASTFLAGVARIABLE(LuauFixFunctionNameStartPosition) LUAU_FASTFLAGVARIABLE(LuauExtendStatEndPosWithSemicolon) -LUAU_FASTFLAGVARIABLE(LuauStoreCSTData) +LUAU_FASTFLAGVARIABLE(LuauStoreCSTData2) LUAU_FASTFLAGVARIABLE(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) LUAU_FASTFLAGVARIABLE(LuauAstTypeGroup3) LUAU_FASTFLAGVARIABLE(ParserNoErrorLimit) LUAU_FASTFLAGVARIABLE(LuauFixDoBlockEndLocation) -LUAU_FASTFLAGVARIABLE(LuauParseOptionalAsNode) +LUAU_FASTFLAGVARIABLE(LuauParseOptionalAsNode2) +LUAU_FASTFLAGVARIABLE(LuauParseStringIndexer) +LUAU_FASTFLAGVARIABLE(LuauTypeFunResultInAutocomplete) +LUAU_FASTFLAGVARIABLE(LuauDeprecatedAttribute) +LUAU_FASTFLAGVARIABLE(LuauFixFunctionWithAttributesStartLocation) +LUAU_DYNAMIC_FASTFLAGVARIABLE(DebugLuauReportReturnTypeVariadicWithTypeSuffix, false) + +// Clip with DebugLuauReportReturnTypeVariadicWithTypeSuffix +bool luau_telemetry_parsed_return_type_variadic_with_type_suffix = false; namespace Luau { @@ -38,7 +45,12 @@ struct AttributeEntry AstAttr::Type type; }; -AttributeEntry kAttributeEntries[] = {{"@checked", AstAttr::Type::Checked}, {"@native", AstAttr::Type::Native}, {nullptr, AstAttr::Type::Checked}}; +AttributeEntry kAttributeEntries[] = { + {"@checked", AstAttr::Type::Checked}, + {"@native", AstAttr::Type::Native}, + {"@deprecated", AstAttr::Type::Deprecated}, + {nullptr, AstAttr::Type::Checked} +}; ParseError::ParseError(const Location& location, const std::string& message) : location(location) @@ -520,7 +532,7 @@ AstStat* Parser::parseRepeat() restoreLocals(localsBegin); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstStatRepeat* node = allocator.alloc(Location(start, cond->location), cond, body, hasUntil); if (options.storeCstData) @@ -550,7 +562,7 @@ AstStat* Parser::parseDo() if (FFlag::LuauFixDoBlockEndLocation && body->hasEnd) body->location.end = endLocation.end; - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) cstNodeMap[body] = allocator.alloc(endLocation.begin); return body; @@ -633,7 +645,7 @@ AstStat* Parser::parseFor() bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); body->hasEnd = hasEnd; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstStatFor* node = allocator.alloc(Location(start, end), var, from, to, step, body, hasDo, matchDo.location); if (options.storeCstData) @@ -653,7 +665,7 @@ AstStat* Parser::parseFor() if (lexer.current().type == ',') { - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) { Position initialCommaPosition = lexer.current().location.begin; nextLexeme(); @@ -672,7 +684,7 @@ AstStat* Parser::parseFor() TempVector values(scratchExpr); TempVector valuesCommaPositions(scratchPosition); - parseExprList(values, (FFlag::LuauStoreCSTData && options.storeCstData) ? &valuesCommaPositions : nullptr); + parseExprList(values, (FFlag::LuauStoreCSTData2 && options.storeCstData) ? &valuesCommaPositions : nullptr); Lexeme matchDo = lexer.current(); bool hasDo = expectAndConsume(Lexeme::ReservedDo, "for loop"); @@ -697,7 +709,7 @@ AstStat* Parser::parseFor() bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); body->hasEnd = hasEnd; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstStatForIn* node = allocator.alloc(Location(start, end), copy(vars), copy(values), body, hasIn, inLocation, hasDo, matchDo.location); @@ -713,7 +725,7 @@ AstStat* Parser::parseFor() } // funcname ::= Name {`.' Name} [`:' Name] -AstExpr* Parser::parseFunctionName(Location start_DEPRECATED, bool& hasself, AstName& debugname) +AstExpr* Parser::parseFunctionName(bool& hasself, AstName& debugname) { if (lexer.current().type == Lexeme::Name) debugname = AstName(lexer.current().name); @@ -734,7 +746,7 @@ AstExpr* Parser::parseFunctionName(Location start_DEPRECATED, bool& hasself, Ast debugname = name.name; expr = allocator.alloc( - Location(FFlag::LuauFixFunctionNameStartPosition ? expr->location : start_DEPRECATED, name.location), + Location(expr->location, name.location), expr, name.name, name.location, @@ -760,7 +772,7 @@ AstExpr* Parser::parseFunctionName(Location start_DEPRECATED, bool& hasself, Ast debugname = name.name; expr = allocator.alloc( - Location(FFlag::LuauFixFunctionNameStartPosition ? expr->location : start_DEPRECATED, name.location), + Location(expr->location, name.location), expr, name.name, name.location, @@ -779,12 +791,18 @@ AstStat* Parser::parseFunctionStat(const AstArray& attributes) { Location start = lexer.current().location; + if (FFlag::LuauFixFunctionWithAttributesStartLocation) + { + if (attributes.size > 0) + start = attributes.data[0]->location; + } + Lexeme matchFunction = lexer.current(); nextLexeme(); bool hasself = false; AstName debugname; - AstExpr* expr = parseFunctionName(start, hasself, debugname); + AstExpr* expr = parseFunctionName(hasself, debugname); matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; @@ -792,7 +810,17 @@ AstStat* Parser::parseFunctionStat(const AstArray& attributes) matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; - return allocator.alloc(Location(start, body->location), expr, body); + if (FFlag::LuauStoreCSTData2 && FFlag::LuauFixFunctionWithAttributesStartLocation) + { + AstStatFunction* node = allocator.alloc(Location(start, body->location), expr, body); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(matchFunction.location.begin); + return node; + } + else + { + return allocator.alloc(Location(start, body->location), expr, body); + } } @@ -814,7 +842,7 @@ std::pair Parser::validateAttribute(const char* attributeNa } } - if (!found) + if (!found || (!FFlag::LuauDeprecatedAttribute && type == AstAttr::Type::Deprecated)) { if (strlen(attributeName) == 1) report(lexer.current().location, "Attribute name is missing"); @@ -907,6 +935,13 @@ AstStat* Parser::parseLocal(const AstArray& attributes) { Location start = lexer.current().location; + if (FFlag::LuauFixFunctionWithAttributesStartLocation) + { + if (attributes.size > 0) + start = attributes.data[0]->location; + } + + Position localKeywordPosition = lexer.current().location.begin; nextLexeme(); // local if (lexer.current().type == Lexeme::ReservedFunction) @@ -930,11 +965,11 @@ AstStat* Parser::parseLocal(const AstArray& attributes) Location location{start.begin, body->location.end}; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstStatLocalFunction* node = allocator.alloc(location, var, body); if (options.storeCstData) - cstNodeMap[node] = allocator.alloc(functionKeywordPosition); + cstNodeMap[node] = allocator.alloc(localKeywordPosition, functionKeywordPosition); return node; } else @@ -959,7 +994,7 @@ AstStat* Parser::parseLocal(const AstArray& attributes) TempVector names(scratchBinding); AstArray varsCommaPositions; - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) parseBindingList(names, false, &varsCommaPositions); else parseBindingList(names); @@ -979,7 +1014,7 @@ AstStat* Parser::parseLocal(const AstArray& attributes) nextLexeme(); - parseExprList(values, (FFlag::LuauStoreCSTData && options.storeCstData) ? &valuesCommaPositions : nullptr); + parseExprList(values, (FFlag::LuauStoreCSTData2 && options.storeCstData) ? &valuesCommaPositions : nullptr); } for (size_t i = 0; i < names.size(); ++i) @@ -987,7 +1022,7 @@ AstStat* Parser::parseLocal(const AstArray& attributes) Location end = values.empty() ? lexer.previousLocation() : values.back()->location; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstStatLocal* node = allocator.alloc(Location(start, end), copy(vars), copy(values), equalsSignLocation); if (options.storeCstData) @@ -1012,11 +1047,11 @@ AstStat* Parser::parseReturn() TempVector commaPositions(scratchPosition); if (!blockFollow(lexer.current()) && lexer.current().type != ';') - parseExprList(list, (FFlag::LuauStoreCSTData && options.storeCstData) ? &commaPositions : nullptr); + parseExprList(list, (FFlag::LuauStoreCSTData2 && options.storeCstData) ? &commaPositions : nullptr); Location end = list.empty() ? start : list.back()->location; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstStatReturn* node = allocator.alloc(Location(start, end), copy(list)); if (options.storeCstData) @@ -1049,7 +1084,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t Position genericsOpenPosition{0, 0}; AstArray genericsCommaPositions; Position genericsClosePosition{0, 0}; - auto [generics, genericPacks] = FFlag::LuauStoreCSTData && options.storeCstData + auto [generics, genericPacks] = FFlag::LuauStoreCSTData2 && options.storeCstData ? parseGenericTypeList( /* withDefaultValues= */ true, &genericsOpenPosition, &genericsCommaPositions, &genericsClosePosition ) @@ -1060,7 +1095,7 @@ AstStat* Parser::parseTypeAlias(const Location& start, bool exported, Position t AstType* type = parseType(); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstStatTypeAlias* node = allocator.alloc(Location(start, type->location), name->name, name->location, generics, genericPacks, type, exported); @@ -1082,6 +1117,8 @@ AstStat* Parser::parseTypeFunction(const Location& start, bool exported, Positio Lexeme matchFn = lexer.current(); nextLexeme(); + size_t errorsAtStart = FFlag::LuauTypeFunResultInAutocomplete ? parseErrors.size() : 0; + // parse the name of the type function std::optional fnName = parseNameOpt("type function name"); if (!fnName) @@ -1098,21 +1135,88 @@ AstStat* Parser::parseTypeFunction(const Location& start, bool exported, Positio matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; - if (FFlag::LuauStoreCSTData) + bool hasErrors = FFlag::LuauTypeFunResultInAutocomplete ? parseErrors.size() > errorsAtStart : false; + + if (FFlag::LuauStoreCSTData2) { AstStatTypeFunction* node = - allocator.alloc(Location(start, body->location), fnName->name, fnName->location, body, exported); + allocator.alloc(Location(start, body->location), fnName->name, fnName->location, body, exported, hasErrors); if (options.storeCstData) cstNodeMap[node] = allocator.alloc(typeKeywordPosition, matchFn.location.begin); return node; } else { - return allocator.alloc(Location(start, body->location), fnName->name, fnName->location, body, exported); + return allocator.alloc(Location(start, body->location), fnName->name, fnName->location, body, exported, hasErrors); } } -AstDeclaredClassProp Parser::parseDeclaredClassMethod() +AstDeclaredClassProp Parser::parseDeclaredClassMethod(const AstArray& attributes) +{ + LUAU_ASSERT(FFlag::LuauDeprecatedAttribute); + + Location start = lexer.current().location; + + nextLexeme(); + + Name fnName = parseName("function name"); + + // TODO: generic method declarations CLI-39909 + AstArray generics; + AstArray genericPacks; + generics.size = 0; + generics.data = nullptr; + genericPacks.size = 0; + genericPacks.data = nullptr; + + MatchLexeme matchParen = lexer.current(); + expectAndConsume('(', "function parameter list start"); + + TempVector args(scratchBinding); + + bool vararg = false; + Location varargLocation; + AstTypePack* varargAnnotation = nullptr; + if (lexer.current().type != ')') + std::tie(vararg, varargLocation, varargAnnotation) = parseBindingList(args, /* allowDot3 */ true); + + expectMatchAndConsume(')', matchParen); + + AstTypeList retTypes = parseOptionalReturnType().value_or(AstTypeList{copy(nullptr, 0), nullptr}); + Location end = lexer.previousLocation(); + + TempVector vars(scratchType); + TempVector> varNames(scratchOptArgName); + + if (args.size() == 0 || args[0].name.name != "self" || args[0].annotation != nullptr) + { + return AstDeclaredClassProp{ + fnName.name, fnName.location, reportTypeError(Location(start, end), {}, "'self' must be present as the unannotated first parameter"), true + }; + } + + // Skip the first index. + for (size_t i = 1; i < args.size(); ++i) + { + varNames.push_back(AstArgumentName{args[i].name.name, args[i].name.location}); + + if (args[i].annotation) + vars.push_back(args[i].annotation); + else + vars.push_back(reportTypeError(Location(start, end), {}, "All declaration parameters aside from 'self' must be annotated")); + } + + if (vararg && !varargAnnotation) + report(start, "All declaration parameters aside from 'self' must be annotated"); + + AstType* fnType = allocator.alloc( + Location(start, end), attributes, generics, genericPacks, AstTypeList{copy(vars), varargAnnotation}, copy(varNames), retTypes + ); + + return AstDeclaredClassProp{fnName.name, fnName.location, fnType, true, Location(start, end)}; +} + +AstDeclaredClassProp Parser::parseDeclaredClassMethod_DEPRECATED() { Location start = lexer.current().location; @@ -1260,75 +1364,178 @@ AstStat* Parser::parseDeclaration(const Location& start, const AstArray attributes{nullptr, 0}; + + if (FFlag::LuauDeprecatedAttribute && (lexer.current().type == Lexeme::Attribute)) { - props.push_back(parseDeclaredClassMethod()); + attributes = Parser::parseAttributes(); + + if (lexer.current().type != Lexeme::ReservedFunction) + return reportStatError( + lexer.current().location, + {}, + {}, + "Expected a method type declaration after attribute, but got %s instead", + lexer.current().toString().c_str() + ); } - else if (lexer.current().type == '[' && (lexer.lookahead().type == Lexeme::RawString || lexer.lookahead().type == Lexeme::QuotedString)) + + if (FFlag::LuauParseStringIndexer) { - const Lexeme begin = lexer.current(); - nextLexeme(); // [ - - const Location nameBegin = lexer.current().location; - std::optional> chars = parseCharArray(); - - const Location nameEnd = lexer.previousLocation(); - - expectMatchAndConsume(']', begin); - expectAndConsume(':', "property type annotation"); - AstType* type = parseType(); - - // since AstName contains a char*, it can't contain null - bool containsNull = chars && (memchr(chars->data, 0, chars->size) != nullptr); - - if (chars && !containsNull) + // There are two possibilities: Either it's a property or a function. + if (lexer.current().type == Lexeme::ReservedFunction) { - props.push_back(AstDeclaredClassProp{ - AstName(chars->data), Location(nameBegin, nameEnd), type, false, Location(begin.location, lexer.previousLocation()) - }); + if (FFlag::LuauDeprecatedAttribute) + props.push_back(parseDeclaredClassMethod(attributes)); + else + props.push_back(parseDeclaredClassMethod_DEPRECATED()); + } + else if (lexer.current().type == '[') + { + const Lexeme begin = lexer.current(); + nextLexeme(); // [ + + if ((lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString) && lexer.lookahead().type == ']') + { + const Location nameBegin = lexer.current().location; + std::optional> chars = parseCharArray(); + + const Location nameEnd = lexer.previousLocation(); + + expectMatchAndConsume(']', begin); + expectAndConsume(':', "property type annotation"); + AstType* type = parseType(); + + // since AstName contains a char*, it can't contain null + bool containsNull = chars && (memchr(chars->data, 0, chars->size) != nullptr); + + if (chars && !containsNull) + { + props.push_back(AstDeclaredClassProp{ + AstName(chars->data), Location(nameBegin, nameEnd), type, false, Location(begin.location, lexer.previousLocation()) + }); + } + else + { + report(begin.location, "String literal contains malformed escape sequence or \\0"); + } + } + else if (indexer) + { + // maybe we don't need to parse the entire badIndexer... + // however, we either have { or [ to lint, not the entire table type or the bad indexer. + AstTableIndexer* badIndexer; + if (FFlag::LuauStoreCSTData2) + badIndexer = parseTableIndexer(AstTableAccess::ReadWrite, std::nullopt, begin).node; + else + badIndexer = parseTableIndexer_DEPRECATED(AstTableAccess::ReadWrite, std::nullopt, begin); + + // we lose all additional indexer expressions from the AST after error recovery here + report(badIndexer->location, "Cannot have more than one class indexer"); + } + else + { + if (FFlag::LuauStoreCSTData2) + indexer = parseTableIndexer(AstTableAccess::ReadWrite, std::nullopt, begin).node; + else + indexer = parseTableIndexer_DEPRECATED(AstTableAccess::ReadWrite, std::nullopt, begin); + } } else { - report(begin.location, "String literal contains malformed escape sequence or \\0"); - } - } - else if (lexer.current().type == '[') - { - if (indexer) - { - // maybe we don't need to parse the entire badIndexer... - // however, we either have { or [ to lint, not the entire table type or the bad indexer. - AstTableIndexer* badIndexer; - if (FFlag::LuauStoreCSTData) - badIndexer = parseTableIndexer(AstTableAccess::ReadWrite, std::nullopt).node; - else - badIndexer = parseTableIndexer_DEPRECATED(AstTableAccess::ReadWrite, std::nullopt); + Location propStart = lexer.current().location; + std::optional propName = parseNameOpt("property name"); - // we lose all additional indexer expressions from the AST after error recovery here - report(badIndexer->location, "Cannot have more than one class indexer"); - } - else - { - if (FFlag::LuauStoreCSTData) - indexer = parseTableIndexer(AstTableAccess::ReadWrite, std::nullopt).node; - else - indexer = parseTableIndexer_DEPRECATED(AstTableAccess::ReadWrite, std::nullopt); + if (!propName) + break; + + expectAndConsume(':', "property type annotation"); + AstType* propType = parseType(); + props.push_back( + AstDeclaredClassProp{propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation())} + ); } } else { - Location propStart = lexer.current().location; - std::optional propName = parseNameOpt("property name"); + // There are two possibilities: Either it's a property or a function. + if (lexer.current().type == Lexeme::ReservedFunction) + { + if (FFlag::LuauDeprecatedAttribute) + props.push_back(parseDeclaredClassMethod(attributes)); + else + props.push_back(parseDeclaredClassMethod_DEPRECATED()); + } + else if (lexer.current().type == '[' && + (lexer.lookahead().type == Lexeme::RawString || lexer.lookahead().type == Lexeme::QuotedString)) + { + const Lexeme begin = lexer.current(); + nextLexeme(); // [ - if (!propName) - break; + const Location nameBegin = lexer.current().location; + std::optional> chars = parseCharArray(); - expectAndConsume(':', "property type annotation"); - AstType* propType = parseType(); - props.push_back( - AstDeclaredClassProp{propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation())} - ); + const Location nameEnd = lexer.previousLocation(); + + expectMatchAndConsume(']', begin); + expectAndConsume(':', "property type annotation"); + AstType* type = parseType(); + + // since AstName contains a char*, it can't contain null + bool containsNull = chars && (memchr(chars->data, 0, chars->size) != nullptr); + + if (chars && !containsNull) + { + props.push_back(AstDeclaredClassProp{ + AstName(chars->data), Location(nameBegin, nameEnd), type, false, Location(begin.location, lexer.previousLocation()) + }); + } + else + { + report(begin.location, "String literal contains malformed escape sequence or \\0"); + } + } + else if (lexer.current().type == '[') + { + if (indexer) + { + // maybe we don't need to parse the entire badIndexer... + // however, we either have { or [ to lint, not the entire table type or the bad indexer. + AstTableIndexer* badIndexer; + if (FFlag::LuauStoreCSTData2) + // the last param in the parseTableIndexer is ignored since FFlagLuauParseStringIndexer is false here + badIndexer = parseTableIndexer(AstTableAccess::ReadWrite, std::nullopt, lexer.current()).node; + else + // the last param in the parseTableIndexer is ignored since FFlagLuauParseStringIndexer is false here + badIndexer = parseTableIndexer_DEPRECATED(AstTableAccess::ReadWrite, std::nullopt, lexer.current()); + + // we lose all additional indexer expressions from the AST after error recovery here + report(badIndexer->location, "Cannot have more than one class indexer"); + } + else + { + if (FFlag::LuauStoreCSTData2) + // the last param in the parseTableIndexer is ignored since FFlagLuauParseStringIndexer is false here + indexer = parseTableIndexer(AstTableAccess::ReadWrite, std::nullopt, lexer.current()).node; + else + // the last param in the parseTableIndexer is ignored since FFlagLuauParseStringIndexer is false here + indexer = parseTableIndexer_DEPRECATED(AstTableAccess::ReadWrite, std::nullopt, lexer.current()); + } + } + else + { + Location propStart = lexer.current().location; + std::optional propName = parseNameOpt("property name"); + + if (!propName) + break; + + expectAndConsume(':', "property type annotation"); + AstType* propType = parseType(); + props.push_back( + AstDeclaredClassProp{propName->name, propName->location, propType, false, Location(propStart, lexer.previousLocation())} + ); + } } } @@ -1367,7 +1574,7 @@ AstStat* Parser::parseAssignment(AstExpr* initial) while (lexer.current().type == ',') { - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) varsCommaPositions.push_back(lexer.current().location.begin); nextLexeme(); @@ -1384,9 +1591,9 @@ AstStat* Parser::parseAssignment(AstExpr* initial) TempVector values(scratchExprAux); TempVector valuesCommaPositions(scratchPosition); - parseExprList(values, FFlag::LuauStoreCSTData && options.storeCstData ? &valuesCommaPositions : nullptr); + parseExprList(values, FFlag::LuauStoreCSTData2 && options.storeCstData ? &valuesCommaPositions : nullptr); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstStatAssign* node = allocator.alloc(Location(initial->location, values.back()->location), copy(vars), copy(values)); cstNodeMap[node] = allocator.alloc(copy(varsCommaPositions), equalsPosition, copy(valuesCommaPositions)); @@ -1411,7 +1618,7 @@ AstStat* Parser::parseCompoundAssignment(AstExpr* initial, AstExprBinary::Op op) AstExpr* value = parseExpr(); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstStatCompoundAssign* node = allocator.alloc(Location(initial->location, value->location), op, initial, value); if (options.storeCstData) @@ -1451,9 +1658,15 @@ std::pair Parser::parseFunctionBody( { Location start = matchFunction.location; - auto* cstNode = FFlag::LuauStoreCSTData && options.storeCstData ? allocator.alloc() : nullptr; + if (FFlag::LuauFixFunctionWithAttributesStartLocation) + { + if (attributes.size > 0) + start = attributes.data[0]->location; + } - auto [generics, genericPacks] = FFlag::LuauStoreCSTData && cstNode ? parseGenericTypeList( + auto* cstNode = FFlag::LuauStoreCSTData2 && options.storeCstData ? allocator.alloc() : nullptr; + + auto [generics, genericPacks] = FFlag::LuauStoreCSTData2 && cstNode ? parseGenericTypeList( /* withDefaultValues= */ false, &cstNode->openGenericsPosition, &cstNode->genericsCommaPositions, @@ -1524,7 +1737,7 @@ std::pair Parser::parseFunctionBody( bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchFunction); body->hasEnd = hasEnd; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstExprFunction* node = allocator.alloc( Location(start, end), @@ -1543,7 +1756,10 @@ std::pair Parser::parseFunctionBody( argLocation ); if (options.storeCstData) + { + cstNode->functionKeywordPosition = matchFunction.location.begin; cstNodeMap[node] = cstNode; + } return {node, funLocal}; } @@ -1578,7 +1794,7 @@ void Parser::parseExprList(TempVector& result, TempVector* c while (lexer.current().type == ',') { - if (FFlag::LuauStoreCSTData && commaPositions) + if (FFlag::LuauStoreCSTData2 && commaPositions) commaPositions->push_back(lexer.current().location.begin); nextLexeme(); @@ -1610,7 +1826,7 @@ std::tuple Parser::parseBindingList(TempVector localCommaPositions(scratchPosition); - if (FFlag::LuauStoreCSTData && commaPositions && initialCommaPosition) + if (FFlag::LuauStoreCSTData2 && commaPositions && initialCommaPosition) localCommaPositions.push_back(*initialCommaPosition); while (true) @@ -1627,7 +1843,7 @@ std::tuple Parser::parseBindingList(TempVector Parser::parseBindingList(TempVectorsize() < result.size()) nameColonPositions->push_back({}); @@ -1686,7 +1902,7 @@ AstTypePack* Parser::parseTypeList( resultNames.push_back(AstArgumentName{AstName(lexer.current().name), lexer.current().location}); nextLexeme(); - if (FFlag::LuauStoreCSTData && nameColonPositions) + if (FFlag::LuauStoreCSTData2 && nameColonPositions) nameColonPositions->push_back(lexer.current().location.begin); expectAndConsume(':'); } @@ -1694,7 +1910,7 @@ AstTypePack* Parser::parseTypeList( { // If we have a type with named arguments, provide elements for all types resultNames.push_back({}); - if (FFlag::LuauStoreCSTData && nameColonPositions) + if (FFlag::LuauStoreCSTData2 && nameColonPositions) nameColonPositions->push_back({}); } @@ -1702,7 +1918,7 @@ AstTypePack* Parser::parseTypeList( if (lexer.current().type != ',') break; - if (FFlag::LuauStoreCSTData && commaPositions) + if (FFlag::LuauStoreCSTData2 && commaPositions) commaPositions->push_back(lexer.current().location.begin); nextLexeme(); @@ -1723,7 +1939,7 @@ std::optional Parser::parseOptionalReturnType(Position* returnSpeci if (lexer.current().type == Lexeme::SkinnyArrow) report(lexer.current().location, "Function return type annotations are written after ':' instead of '->'"); - if (FFlag::LuauStoreCSTData && returnSpecifierPosition) + if (FFlag::LuauStoreCSTData2 && returnSpecifierPosition) *returnSpecifierPosition = lexer.current().location.begin; nextLexeme(); @@ -1802,6 +2018,10 @@ std::pair Parser::parseReturnType() AstType* inner = varargAnnotation == nullptr ? allocator.alloc(location, result[0]) : result[0]; AstType* returnType = parseTypeSuffix(inner, begin.location); + if (DFFlag::DebugLuauReportReturnTypeVariadicWithTypeSuffix && varargAnnotation != nullptr && + (returnType->is() || returnType->is())) + luau_telemetry_parsed_return_type_variadic_with_type_suffix = true; + // If parseType parses nothing, then returnType->location.end only points at the last non-type-pack // type to successfully parse. We need the span of the whole annotation. Position endPos = result.size() == 1 ? location.end : returnType->location.end; @@ -1815,6 +2035,10 @@ std::pair Parser::parseReturnType() { AstType* returnType = parseTypeSuffix(result[0], innerBegin); + if (DFFlag::DebugLuauReportReturnTypeVariadicWithTypeSuffix && varargAnnotation != nullptr && + (returnType->is() || returnType->is())) + luau_telemetry_parsed_return_type_variadic_with_type_suffix = true; + // If parseType parses nothing, then returnType->location.end only points at the last non-type-pack // type to successfully parse. We need the span of the whole annotation. Position endPos = result.size() == 1 ? location.end : returnType->location.end; @@ -1833,7 +2057,7 @@ std::pair Parser::parseReturnType() std::pair Parser::extractStringDetails() { - LUAU_ASSERT(FFlag::LuauStoreCSTData); + LUAU_ASSERT(FFlag::LuauStoreCSTData2); CstExprConstantString::QuoteStyle style; unsigned int blockDepth = 0; @@ -1861,10 +2085,13 @@ std::pair Parser::extractString } // TableIndexer ::= `[' Type `]' `:' Type -Parser::TableIndexerResult Parser::parseTableIndexer(AstTableAccess access, std::optional accessLocation) +Parser::TableIndexerResult Parser::parseTableIndexer(AstTableAccess access, std::optional accessLocation, Lexeme begin) { - const Lexeme begin = lexer.current(); - nextLexeme(); // [ + if (!FFlag::LuauParseStringIndexer) + { + begin = lexer.current(); + nextLexeme(); // [ + } AstType* index = parseType(); @@ -1884,11 +2111,14 @@ Parser::TableIndexerResult Parser::parseTableIndexer(AstTableAccess access, std: }; } -// Remove with FFlagLuauStoreCSTData -AstTableIndexer* Parser::parseTableIndexer_DEPRECATED(AstTableAccess access, std::optional accessLocation) +// Remove with FFlagLuauStoreCSTData2 +AstTableIndexer* Parser::parseTableIndexer_DEPRECATED(AstTableAccess access, std::optional accessLocation, Lexeme begin) { - const Lexeme begin = lexer.current(); - nextLexeme(); // [ + if (!FFlag::LuauParseStringIndexer) + { + begin = lexer.current(); + nextLexeme(); // [ + } AstType* index = parseType(); @@ -1941,116 +2171,240 @@ AstType* Parser::parseTableType(bool inDeclarationContext) } } - if (lexer.current().type == '[' && (lexer.lookahead().type == Lexeme::RawString || lexer.lookahead().type == Lexeme::QuotedString)) + if (FFlag::LuauParseStringIndexer) { - const Lexeme begin = lexer.current(); - nextLexeme(); // [ - - CstExprConstantString::QuoteStyle style; - unsigned int blockDepth = 0; - if (FFlag::LuauStoreCSTData && options.storeCstData) - std::tie(style, blockDepth) = extractStringDetails(); - - AstArray sourceString; - std::optional> chars = parseCharArray(options.storeCstData ? &sourceString : nullptr); - - Position indexerClosePosition = lexer.current().location.begin; - expectMatchAndConsume(']', begin); - Position colonPosition = lexer.current().location.begin; - expectAndConsume(':', "table field"); - - AstType* type = parseType(); - - // since AstName contains a char*, it can't contain null - bool containsNull = chars && (memchr(chars->data, 0, chars->size) != nullptr); - - if (chars && !containsNull) + if (lexer.current().type == '[') { - props.push_back(AstTableProp{AstName(chars->data), begin.location, type, access, accessLocation}); - if (FFlag::LuauStoreCSTData && options.storeCstData) + const Lexeme begin = lexer.current(); + nextLexeme(); // [ + + if ((lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString) && lexer.lookahead().type == ']') + { + CstExprConstantString::QuoteStyle style; + unsigned int blockDepth = 0; + if (FFlag::LuauStoreCSTData2 && options.storeCstData) + std::tie(style, blockDepth) = extractStringDetails(); + + AstArray sourceString; + std::optional> chars = parseCharArray(options.storeCstData ? &sourceString : nullptr); + + Position indexerClosePosition = lexer.current().location.begin; + expectMatchAndConsume(']', begin); + Position colonPosition = lexer.current().location.begin; + expectAndConsume(':', "table field"); + + AstType* type = parseType(); + + // since AstName contains a char*, it can't contain null + bool containsNull = chars && (memchr(chars->data, 0, chars->size) != nullptr); + + if (chars && !containsNull) + { + props.push_back(AstTableProp{AstName(chars->data), begin.location, type, access, accessLocation}); + if (FFlag::LuauStoreCSTData2 && options.storeCstData) + cstItems.push_back(CstTypeTable::Item{ + CstTypeTable::Item::Kind::StringProperty, + begin.location.begin, + indexerClosePosition, + colonPosition, + tableSeparator(), + lexer.current().location.begin, + allocator.alloc(sourceString, style, blockDepth) + }); + } + else + report(begin.location, "String literal contains malformed escape sequence or \\0"); + } + else + { + if (indexer) + { + // maybe we don't need to parse the entire badIndexer... + // however, we either have { or [ to lint, not the entire table type or the bad indexer. + AstTableIndexer* badIndexer; + if (FFlag::LuauStoreCSTData2) + badIndexer = parseTableIndexer(access, accessLocation, begin).node; + else + badIndexer = parseTableIndexer_DEPRECATED(access, accessLocation, begin); + + // we lose all additional indexer expressions from the AST after error recovery here + report(badIndexer->location, "Cannot have more than one table indexer"); + } + else + { + if (FFlag::LuauStoreCSTData2) + { + auto tableIndexerResult = parseTableIndexer(access, accessLocation, begin); + indexer = tableIndexerResult.node; + if (options.storeCstData) + cstItems.push_back(CstTypeTable::Item{ + CstTypeTable::Item::Kind::Indexer, + tableIndexerResult.indexerOpenPosition, + tableIndexerResult.indexerClosePosition, + tableIndexerResult.colonPosition, + tableSeparator(), + lexer.current().location.begin, + }); + } + else + { + indexer = parseTableIndexer_DEPRECATED(access, accessLocation, begin); + } + } + } + } + else if (props.empty() && !indexer && !(lexer.current().type == Lexeme::Name && lexer.lookahead().type == ':')) + { + AstType* type = parseType(); + + // array-like table type: {T} desugars into {[number]: T} + isArray = true; + AstType* index = allocator.alloc(type->location, std::nullopt, nameNumber, std::nullopt, type->location); + indexer = allocator.alloc(AstTableIndexer{index, type, type->location, access, accessLocation}); + + break; + } + else + { + std::optional name = parseNameOpt("table field"); + + if (!name) + break; + + Position colonPosition = lexer.current().location.begin; + expectAndConsume(':', "table field"); + + AstType* type = parseType(inDeclarationContext); + + props.push_back(AstTableProp{name->name, name->location, type, access, accessLocation}); + if (FFlag::LuauStoreCSTData2 && options.storeCstData) cstItems.push_back(CstTypeTable::Item{ - CstTypeTable::Item::Kind::StringProperty, - begin.location.begin, - indexerClosePosition, + CstTypeTable::Item::Kind::Property, + Position{0, 0}, + Position{0, 0}, colonPosition, tableSeparator(), - lexer.current().location.begin, - allocator.alloc(sourceString, style, blockDepth) + lexer.current().location.begin }); } - else - report(begin.location, "String literal contains malformed escape sequence or \\0"); - } - else if (lexer.current().type == '[') - { - if (indexer) - { - // maybe we don't need to parse the entire badIndexer... - // however, we either have { or [ to lint, not the entire table type or the bad indexer. - AstTableIndexer* badIndexer; - if (FFlag::LuauStoreCSTData) - badIndexer = parseTableIndexer(access, accessLocation).node; - else - badIndexer = parseTableIndexer_DEPRECATED(access, accessLocation); - - // we lose all additional indexer expressions from the AST after error recovery here - report(badIndexer->location, "Cannot have more than one table indexer"); - } - else - { - if (FFlag::LuauStoreCSTData) - { - auto tableIndexerResult = parseTableIndexer(access, accessLocation); - indexer = tableIndexerResult.node; - if (options.storeCstData) - cstItems.push_back(CstTypeTable::Item{ - CstTypeTable::Item::Kind::Indexer, - tableIndexerResult.indexerOpenPosition, - tableIndexerResult.indexerClosePosition, - tableIndexerResult.colonPosition, - tableSeparator(), - lexer.current().location.begin, - }); - } - else - { - indexer = parseTableIndexer_DEPRECATED(access, accessLocation); - } - } - } - else if (props.empty() && !indexer && !(lexer.current().type == Lexeme::Name && lexer.lookahead().type == ':')) - { - AstType* type = parseType(); - - // array-like table type: {T} desugars into {[number]: T} - isArray = true; - AstType* index = allocator.alloc(type->location, std::nullopt, nameNumber, std::nullopt, type->location); - indexer = allocator.alloc(AstTableIndexer{index, type, type->location, access, accessLocation}); - - break; } else { - std::optional name = parseNameOpt("table field"); + if (lexer.current().type == '[' && (lexer.lookahead().type == Lexeme::RawString || lexer.lookahead().type == Lexeme::QuotedString)) + { + const Lexeme begin = lexer.current(); + nextLexeme(); // [ + + CstExprConstantString::QuoteStyle style; + unsigned int blockDepth = 0; + if (FFlag::LuauStoreCSTData2 && options.storeCstData) + std::tie(style, blockDepth) = extractStringDetails(); + + AstArray sourceString; + std::optional> chars = parseCharArray(options.storeCstData ? &sourceString : nullptr); + + Position indexerClosePosition = lexer.current().location.begin; + expectMatchAndConsume(']', begin); + Position colonPosition = lexer.current().location.begin; + expectAndConsume(':', "table field"); + + AstType* type = parseType(); + + // since AstName contains a char*, it can't contain null + bool containsNull = chars && (memchr(chars->data, 0, chars->size) != nullptr); + + if (chars && !containsNull) + { + props.push_back(AstTableProp{AstName(chars->data), begin.location, type, access, accessLocation}); + if (FFlag::LuauStoreCSTData2 && options.storeCstData) + cstItems.push_back(CstTypeTable::Item{ + CstTypeTable::Item::Kind::StringProperty, + begin.location.begin, + indexerClosePosition, + colonPosition, + tableSeparator(), + lexer.current().location.begin, + allocator.alloc(sourceString, style, blockDepth) + }); + } + else + report(begin.location, "String literal contains malformed escape sequence or \\0"); + } + else if (lexer.current().type == '[') + { + if (indexer) + { + // maybe we don't need to parse the entire badIndexer... + // however, we either have { or [ to lint, not the entire table type or the bad indexer. + AstTableIndexer* badIndexer; + if (FFlag::LuauStoreCSTData2) + // the last param in the parseTableIndexer is ignored + badIndexer = parseTableIndexer(access, accessLocation, lexer.current()).node; + else + // the last param in the parseTableIndexer is ignored + badIndexer = parseTableIndexer_DEPRECATED(access, accessLocation, lexer.current()); + + // we lose all additional indexer expressions from the AST after error recovery here + report(badIndexer->location, "Cannot have more than one table indexer"); + } + else + { + if (FFlag::LuauStoreCSTData2) + { + // the last param in the parseTableIndexer is ignored + auto tableIndexerResult = parseTableIndexer(access, accessLocation, lexer.current()); + indexer = tableIndexerResult.node; + if (options.storeCstData) + cstItems.push_back(CstTypeTable::Item{ + CstTypeTable::Item::Kind::Indexer, + tableIndexerResult.indexerOpenPosition, + tableIndexerResult.indexerClosePosition, + tableIndexerResult.colonPosition, + tableSeparator(), + lexer.current().location.begin, + }); + } + else + { + // the last param in the parseTableIndexer is ignored + indexer = parseTableIndexer_DEPRECATED(access, accessLocation, lexer.current()); + } + } + } + else if (props.empty() && !indexer && !(lexer.current().type == Lexeme::Name && lexer.lookahead().type == ':')) + { + AstType* type = parseType(); + + // array-like table type: {T} desugars into {[number]: T} + isArray = true; + AstType* index = allocator.alloc(type->location, std::nullopt, nameNumber, std::nullopt, type->location); + indexer = allocator.alloc(AstTableIndexer{index, type, type->location, access, accessLocation}); - if (!name) break; + } + else + { + std::optional name = parseNameOpt("table field"); - Position colonPosition = lexer.current().location.begin; - expectAndConsume(':', "table field"); + if (!name) + break; - AstType* type = parseType(inDeclarationContext); + Position colonPosition = lexer.current().location.begin; + expectAndConsume(':', "table field"); - props.push_back(AstTableProp{name->name, name->location, type, access, accessLocation}); - if (FFlag::LuauStoreCSTData && options.storeCstData) - cstItems.push_back(CstTypeTable::Item{ - CstTypeTable::Item::Kind::Property, - Position{0, 0}, - Position{0, 0}, - colonPosition, - tableSeparator(), - lexer.current().location.begin - }); + AstType* type = parseType(inDeclarationContext); + + props.push_back(AstTableProp{name->name, name->location, type, access, accessLocation}); + if (FFlag::LuauStoreCSTData2 && options.storeCstData) + cstItems.push_back(CstTypeTable::Item{ + CstTypeTable::Item::Kind::Property, + Position{0, 0}, + Position{0, 0}, + colonPosition, + tableSeparator(), + lexer.current().location.begin + }); + } } if (lexer.current().type == ',' || lexer.current().type == ';') @@ -2069,7 +2423,7 @@ AstType* Parser::parseTableType(bool inDeclarationContext) if (!expectMatchAndConsume('}', matchBrace, /* searchForMissing = */ FFlag::LuauErrorRecoveryForTableTypes)) end = lexer.previousLocation(); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstTypeTable* node = allocator.alloc(Location(start, end), copy(props), indexer); if (options.storeCstData) @@ -2095,7 +2449,7 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray Position genericsOpenPosition{0, 0}; AstArray genericsCommaPositions; Position genericsClosePosition{0, 0}; - auto [generics, genericPacks] = FFlag::LuauStoreCSTData && options.storeCstData + auto [generics, genericPacks] = FFlag::LuauStoreCSTData2 && options.storeCstData ? parseGenericTypeList( /* withDefaultValues= */ false, &genericsOpenPosition, &genericsCommaPositions, &genericsClosePosition ) @@ -2115,7 +2469,7 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray if (lexer.current().type != ')') { - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) varargAnnotation = parseTypeList(params, names, &argCommaPositions, &nameColonPositions); else varargAnnotation = parseTypeList(params, names); @@ -2138,7 +2492,7 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray { if (allowPack) { - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstTypePackExplicit* node = allocator.alloc(begin.location, AstTypeList{paramTypes, nullptr}); if (options.storeCstData) @@ -2162,7 +2516,7 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray if (!forceFunctionType && !returnTypeIntroducer && allowPack) { - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstTypePackExplicit* node = allocator.alloc(begin.location, AstTypeList{paramTypes, varargAnnotation}); if (options.storeCstData) @@ -2178,7 +2532,7 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack, const AstArray AstArray> paramNames = copy(names); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { Position returnArrowPosition = lexer.current().location.begin; AstType* node = parseFunctionTypeTail(begin, attributes, generics, genericPacks, paramTypes, paramNames, varargAnnotation); @@ -2254,6 +2608,8 @@ static bool isTypeFollow(Lexeme::Type c) AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) { TempVector parts(scratchType); + TempVector separatorPositions(scratchPosition); + std::optional leadingPosition = std::nullopt; if (type != nullptr) parts.push_back(type); @@ -2262,7 +2618,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) bool isUnion = false; bool isIntersection = false; - // Clip with FFlag::LuauParseOptionalAsNode + // Clip with FFlag::LuauParseOptionalAsNode2 bool hasOptional_DEPRECATED = false; unsigned int optionalCount = 0; @@ -2271,6 +2627,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) while (true) { Lexeme::Type c = lexer.current().type; + Position separatorPosition = lexer.current().location.begin; if (c == '|') { nextLexeme(); @@ -2280,6 +2637,14 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) recursionCounter = oldRecursionCount; isUnion = true; + + if (FFlag::LuauStoreCSTData2 && options.storeCstData) + { + if (type == nullptr && !leadingPosition.has_value()) + leadingPosition = separatorPosition; + else + separatorPositions.push_back(separatorPosition); + } } else if (c == '?') { @@ -2288,7 +2653,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) Location loc = lexer.current().location; nextLexeme(); - if (FFlag::LuauParseOptionalAsNode) + if (FFlag::LuauParseOptionalAsNode2) { parts.push_back(allocator.alloc(Location(loc))); optionalCount++; @@ -2311,6 +2676,14 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) recursionCounter = oldRecursionCount; isIntersection = true; + + if (FFlag::LuauStoreCSTData2 && options.storeCstData) + { + if (type == nullptr && !leadingPosition.has_value()) + leadingPosition = separatorPosition; + else + separatorPositions.push_back(separatorPosition); + } } else if (c == Lexeme::Dot3) { @@ -2320,7 +2693,7 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) else break; - if (FFlag::LuauParseOptionalAsNode) + if (FFlag::LuauParseOptionalAsNode2) { if (parts.size() > unsigned(FInt::LuauTypeLengthLimit) + optionalCount) ParseError::raise(parts.back()->location, "Exceeded allowed type length; simplify your type annotation to make the code compile"); @@ -2354,11 +2727,32 @@ AstType* Parser::parseTypeSuffix(AstType* type, const Location& begin) location.end = parts.back()->location.end; - if (isUnion) - return allocator.alloc(location, copy(parts)); + if (FFlag::LuauStoreCSTData2) + { + if (isUnion) + { + AstTypeUnion* node = allocator.alloc(location, copy(parts)); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(leadingPosition, copy(separatorPositions)); + return node; + } - if (isIntersection) - return allocator.alloc(location, copy(parts)); + if (isIntersection) + { + AstTypeIntersection* node = allocator.alloc(location, copy(parts)); + if (options.storeCstData) + cstNodeMap[node] = allocator.alloc(leadingPosition, copy(separatorPositions)); + return node; + } + } + else + { + if (isUnion) + return allocator.alloc(location, copy(parts)); + + if (isIntersection) + return allocator.alloc(location, copy(parts)); + } LUAU_ASSERT(false); ParseError::raise(begin, "Composite type was not an intersection or union."); @@ -2445,7 +2839,7 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) } else if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString) { - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { CstExprConstantString::QuoteStyle style; unsigned int blockDepth = 0; @@ -2495,7 +2889,7 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) if (lexer.current().type == '.') { - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { prefixPointPosition = lexer.current().location.begin; nextLexeme(); @@ -2530,7 +2924,7 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) expectMatchAndConsume(')', typeofBegin); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstTypeTypeof* node = allocator.alloc(Location(start, end), expr); if (options.storeCstData) @@ -2552,7 +2946,7 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) if (lexer.current().type == '<') { hasParameters = true; - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) parameters = parseTypeParams(¶metersOpeningPosition, ¶metersCommaPositions, ¶metersClosingPosition); else parameters = parseTypeParams(); @@ -2560,7 +2954,7 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext) Location end = lexer.previousLocation(); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstTypeReference* node = allocator.alloc(Location(start, end), prefix, name.name, prefixLocation, name.location, hasParameters, parameters); @@ -2621,7 +3015,7 @@ AstTypePack* Parser::parseVariadicArgumentTypePack() // This will not fail because of the lookahead guard. expectAndConsume(Lexeme::Dot3, "generic type pack annotation"); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstTypePackGeneric* node = allocator.alloc(Location(name.location, end), name.name); if (options.storeCstData) @@ -2659,7 +3053,7 @@ AstTypePack* Parser::parseTypePack() // This will not fail because of the lookahead guard. expectAndConsume(Lexeme::Dot3, "generic type pack annotation"); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstTypePackGeneric* node = allocator.alloc(Location(name.location, end), name.name); if (options.storeCstData) @@ -2853,7 +3247,7 @@ AstExpr* Parser::parseExpr(unsigned int limit) AstExpr* subexpr = parseExpr(unaryPriority); expr = allocator.alloc(Location(start, subexpr->location), *uop, subexpr); - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) cstNodeMap[expr] = allocator.alloc(opPosition); } else @@ -2876,7 +3270,7 @@ AstExpr* Parser::parseExpr(unsigned int limit) AstExpr* next = parseExpr(binaryPriority[*op].right); expr = allocator.alloc(Location(start, next->location), *op, expr, next); - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) cstNodeMap[expr] = allocator.alloc(opPosition); op = parseBinaryOp(lexer.current()); @@ -2983,7 +3377,7 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement) expectMatchAndConsume(']', matchBracket); expr = allocator.alloc(Location(start, end), expr, index); - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) cstNodeMap[expr] = allocator.alloc(matchBracket.position, closeBracketPosition); } else if (lexer.current().type == ':') @@ -3034,7 +3428,7 @@ AstExpr* Parser::parseAssertionExpr() if (lexer.current().type == Lexeme::DoubleColon) { CstExprTypeAssertion* cstNode = nullptr; - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) { Position opPosition = lexer.current().location.begin; cstNode = allocator.alloc(opPosition); @@ -3042,7 +3436,7 @@ AstExpr* Parser::parseAssertionExpr() nextLexeme(); AstType* annotation = parseType(); AstExprTypeAssertion* node = allocator.alloc(Location(start, annotation->location), expr, annotation); - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) cstNodeMap[node] = cstNode; return node; } @@ -3224,14 +3618,14 @@ AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self) TempVector commaPositions(scratchPosition); if (lexer.current().type != ')') - parseExprList(args, (FFlag::LuauStoreCSTData && options.storeCstData) ? &commaPositions : nullptr); + parseExprList(args, (FFlag::LuauStoreCSTData2 && options.storeCstData) ? &commaPositions : nullptr); Location end = lexer.current().location; Position argEnd = end.end; expectMatchAndConsume(')', matchParen); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstExprCall* node = allocator.alloc(Location(func->location, end), func, copy(args), self, Location(argStart, argEnd)); if (options.storeCstData) @@ -3249,7 +3643,7 @@ AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self) AstExpr* expr = parseTableConstructor(); Position argEnd = lexer.previousLocation().end; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstExprCall* node = allocator.alloc(Location(func->location, expr->location), func, copy(&expr, 1), self, Location(argStart, argEnd)); @@ -3267,7 +3661,7 @@ AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self) Location argLocation = lexer.current().location; AstExpr* expr = parseString(); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstExprCall* node = allocator.alloc(Location(func->location, expr->location), func, copy(&expr, 1), self, argLocation); if (options.storeCstData) @@ -3313,7 +3707,7 @@ LUAU_NOINLINE void Parser::reportAmbiguousCallError() std::optional Parser::tableSeparator() { - LUAU_ASSERT(FFlag::LuauStoreCSTData); + LUAU_ASSERT(FFlag::LuauStoreCSTData2); if (lexer.current().type == ',') return CstExprTable::Comma; else if (lexer.current().type == ';') @@ -3358,7 +3752,7 @@ AstExpr* Parser::parseTableConstructor() AstExpr* value = parseExpr(); items.push_back({AstExprTable::Item::General, key, value}); - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) cstItems.push_back({indexerOpenPosition, indexerClosePosition, equalsPosition, tableSeparator(), lexer.current().location.begin}); } else if (lexer.current().type == Lexeme::Name && lexer.lookahead().type == '=') @@ -3379,7 +3773,7 @@ AstExpr* Parser::parseTableConstructor() func->debugname = name.name; items.push_back({AstExprTable::Item::Record, key, value}); - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) cstItems.push_back({std::nullopt, std::nullopt, equalsPosition, tableSeparator(), lexer.current().location.begin}); } else @@ -3387,7 +3781,7 @@ AstExpr* Parser::parseTableConstructor() AstExpr* expr = parseExpr(); items.push_back({AstExprTable::Item::List, nullptr, expr}); - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) cstItems.push_back({std::nullopt, std::nullopt, std::nullopt, tableSeparator(), lexer.current().location.begin}); } @@ -3410,7 +3804,7 @@ AstExpr* Parser::parseTableConstructor() if (!expectMatchAndConsume('}', matchBrace)) end = lexer.previousLocation(); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstExprTable* node = allocator.alloc(Location(start, end), copy(items)); if (options.storeCstData) @@ -3447,7 +3841,7 @@ AstExpr* Parser::parseIfElseExpr() hasElse = true; falseExpr = parseIfElseExpr(); recursionCounter = oldRecursionCount; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) isElseIf = true; } else @@ -3458,7 +3852,7 @@ AstExpr* Parser::parseIfElseExpr() Location end = falseExpr->location; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstExprIfElse* node = allocator.alloc(Location(start, end), condition, hasThen, trueExpr, hasElse, falseExpr); if (options.storeCstData) @@ -3535,7 +3929,7 @@ std::pair, AstArray> Parser::pars if (lexer.current().type == '<') { Lexeme begin = lexer.current(); - if (FFlag::LuauStoreCSTData && openPosition) + if (FFlag::LuauStoreCSTData2 && openPosition) *openPosition = begin.location.begin; nextLexeme(); @@ -3566,7 +3960,7 @@ std::pair, AstArray> Parser::pars { AstTypePack* typePack = parseTypePack(); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstGenericTypePack* node = allocator.alloc(nameLocation, name, typePack); if (options.storeCstData) @@ -3585,7 +3979,7 @@ std::pair, AstArray> Parser::pars if (type) report(type->location, "Expected type pack after '=', got type"); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstGenericTypePack* node = allocator.alloc(nameLocation, name, typePack); if (options.storeCstData) @@ -3603,7 +3997,7 @@ std::pair, AstArray> Parser::pars if (seenDefault) report(lexer.current().location, "Expected default type pack after type pack name"); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstGenericTypePack* node = allocator.alloc(nameLocation, name, nullptr); if (options.storeCstData) @@ -3626,7 +4020,7 @@ std::pair, AstArray> Parser::pars AstType* defaultType = parseType(); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstGenericType* node = allocator.alloc(nameLocation, name, defaultType); if (options.storeCstData) @@ -3643,7 +4037,7 @@ std::pair, AstArray> Parser::pars if (seenDefault) report(lexer.current().location, "Expected default type after type name"); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstGenericType* node = allocator.alloc(nameLocation, name, nullptr); if (options.storeCstData) @@ -3659,7 +4053,7 @@ std::pair, AstArray> Parser::pars if (lexer.current().type == ',') { - if (FFlag::LuauStoreCSTData && commaPositions) + if (FFlag::LuauStoreCSTData2 && commaPositions) localCommaPositions.push_back(lexer.current().location.begin); nextLexeme(); @@ -3673,12 +4067,12 @@ std::pair, AstArray> Parser::pars break; } - if (FFlag::LuauStoreCSTData && closePosition) + if (FFlag::LuauStoreCSTData2 && closePosition) *closePosition = lexer.current().location.begin; expectMatchAndConsume('>', begin); } - if (FFlag::LuauStoreCSTData && commaPositions) + if (FFlag::LuauStoreCSTData2 && commaPositions) *commaPositions = copy(localCommaPositions); AstArray generics = copy(names); @@ -3693,7 +4087,7 @@ AstArray Parser::parseTypeParams(Position* openingPosition, TempV if (lexer.current().type == '<') { Lexeme begin = lexer.current(); - if (FFlag::LuauStoreCSTData && openingPosition) + if (FFlag::LuauStoreCSTData2 && openingPosition) *openingPosition = begin.location.begin; nextLexeme(); @@ -3791,7 +4185,7 @@ AstArray Parser::parseTypeParams(Position* openingPosition, TempV if (lexer.current().type == ',') { - if (FFlag::LuauStoreCSTData && commaPositions) + if (FFlag::LuauStoreCSTData2 && commaPositions) commaPositions->push_back(lexer.current().location.begin); nextLexeme(); } @@ -3799,7 +4193,7 @@ AstArray Parser::parseTypeParams(Position* openingPosition, TempV break; } - if (FFlag::LuauStoreCSTData && closingPosition) + if (FFlag::LuauStoreCSTData2 && closingPosition) *closingPosition = lexer.current().location.begin; expectMatchAndConsume('>', begin); } @@ -3815,7 +4209,7 @@ std::optional> Parser::parseCharArray(AstArray* originalStr ); scratchData.assign(lexer.current().data, lexer.current().getLength()); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { if (originalString) *originalString = copy(scratchData); @@ -3857,7 +4251,7 @@ AstExpr* Parser::parseString() LUAU_ASSERT(false && "Invalid string type"); } - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { CstExprConstantString::QuoteStyle fullStyle; unsigned int blockDepth; @@ -3906,7 +4300,7 @@ AstExpr* Parser::parseInterpString() scratchData.assign(currentLexeme.data, currentLexeme.getLength()); - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) { sourceStrings.push_back(copy(scratchData)); stringPositions.push_back(currentLexeme.location.begin); @@ -3976,7 +4370,7 @@ AstExpr* Parser::parseInterpString() AstArray> stringsArray = copy(strings); AstArray expressionsArray = copy(expressions); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstExprInterpString* node = allocator.alloc(Location{startLocation, endLocation}, stringsArray, expressionsArray); if (options.storeCstData) @@ -3993,7 +4387,7 @@ AstExpr* Parser::parseNumber() scratchData.assign(lexer.current().data, lexer.current().getLength()); AstArray sourceData; - if (FFlag::LuauStoreCSTData && options.storeCstData) + if (FFlag::LuauStoreCSTData2 && options.storeCstData) sourceData = copy(scratchData); // Remove all internal _ - they don't hold any meaning and this allows parsing code to just pass the string pointer to strtod et al @@ -4009,7 +4403,7 @@ AstExpr* Parser::parseNumber() if (result == ConstantNumberParseResult::Malformed) return reportExprError(start, {}, "Malformed number"); - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { AstExprConstantNumber* node = allocator.alloc(start, value, result); if (options.storeCstData) diff --git a/Sources.cmake b/Sources.cmake index fcb84aeb..a40505df 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -169,7 +169,6 @@ target_sources(Luau.CodeGen PRIVATE # Luau.Analysis Sources target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Anyification.h - Analysis/include/Luau/AnyTypeSummary.h Analysis/include/Luau/ApplyTypeFunction.h Analysis/include/Luau/AstJsonEncoder.h Analysis/include/Luau/AstQuery.h @@ -248,7 +247,6 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/VisitType.h Analysis/src/Anyification.cpp - Analysis/src/AnyTypeSummary.cpp Analysis/src/ApplyTypeFunction.cpp Analysis/src/AstJsonEncoder.cpp Analysis/src/AstQuery.cpp @@ -433,7 +431,6 @@ endif() if(TARGET Luau.UnitTest) # Luau.UnitTest Sources target_sources(Luau.UnitTest PRIVATE - tests/AnyTypeSummary.test.cpp tests/AssemblyBuilderA64.test.cpp tests/AssemblyBuilderX64.test.cpp tests/AstJsonEncoder.test.cpp diff --git a/fuzz/proto.cpp b/fuzz/proto.cpp index 9c2ab35c..adba6bdb 100644 --- a/fuzz/proto.cpp +++ b/fuzz/proto.cpp @@ -134,6 +134,7 @@ int registerTypes(Luau::Frontend& frontend, Luau::GlobalTypes& globals, bool for getMutable(vector3MetaType)->props = { {"__add", {makeFunction(arena, nullopt, {vector3InstanceType, vector3InstanceType}, {vector3InstanceType})}}, }; + getMutable(vector3MetaType)->state = TableState::Sealed; globals.globalScope->exportedTypeBindings["Vector3"] = TypeFun{{}, vector3InstanceType}; diff --git a/tests/AnyTypeSummary.test.cpp b/tests/AnyTypeSummary.test.cpp deleted file mode 100644 index a701f2cb..00000000 --- a/tests/AnyTypeSummary.test.cpp +++ /dev/null @@ -1,1038 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/AstQuery.h" -#include "Luau/BuiltinDefinitions.h" -#include "Luau/RequireTracer.h" - -#include "Fixture.h" - -#include "ScopedFlags.h" -#include "doctest.h" - -#include - -using namespace Luau; - -using Pattern = AnyTypeSummary::Pattern; - -LUAU_FASTFLAG(LuauSolverV2) -LUAU_FASTFLAG(DebugLuauFreezeArena) -LUAU_FASTFLAG(DebugLuauMagicTypes) -LUAU_FASTFLAG(StudioReportLuauAny2) -LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope) -LUAU_FASTFLAG(LuauStoreCSTData) -LUAU_FASTFLAG(LuauAstTypeGroup3) -LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment) -LUAU_FASTFLAG(LuauSkipNoRefineDuringRefinement) - - -struct ATSFixture : BuiltinsFixture -{ - - ATSFixture() - { - addGlobalBinding(frontend.globals, "game", builtinTypes->anyType, "@test"); - addGlobalBinding(frontend.globals, "script", builtinTypes->anyType, "@test"); - } -}; - -TEST_SUITE_BEGIN("AnyTypeSummaryTest"); - -TEST_CASE_FIXTURE(ATSFixture, "var_typepack_any") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -type A = (number, string) -> ...any -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::Alias); - CHECK(module->ats.typeInfo[0].node == "type A = (number, string)->( ...any)"); -} - -TEST_CASE_FIXTURE(ATSFixture, "export_alias") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -export type t8 = t0 &((true | any)->('')) -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_ERROR_COUNT(1, result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::Alias); - if (FFlag::LuauStoreCSTData && FFlag::LuauAstTypeGroup3) - { - CHECK(module->ats.typeInfo[0].node == "export type t8 = t0& (( true | any)->(''))"); - } - else if (FFlag::LuauStoreCSTData) - { - CHECK(module->ats.typeInfo[0].node == "export type t8 = t0 &(( true | any)->(''))"); - } - else if (FFlag::LuauAstTypeGroup3) - { - CHECK(module->ats.typeInfo[0].node == "export type t8 = t0& ((true | any)->(''))"); - } - else - { - CHECK(module->ats.typeInfo[0].node == "export type t8 = t0 &((true | any)->(''))"); - } -} - -TEST_CASE_FIXTURE(ATSFixture, "typepacks") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -local function fallible(t: number): ...any - if t > 0 then - return true, t -- should catch this - end - return false, "must be positive" -- should catch this -end -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 3); - CHECK(module->ats.typeInfo[1].code == Pattern::TypePk); - CHECK( - module->ats.typeInfo[0].node == - "local function fallible(t: number): ...any\n if t > 0 then\n return true, t\n end\n return false, 'must be positive'\nend" - ); -} - -TEST_CASE_FIXTURE(ATSFixture, "typepacks_no_ret") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( --- TODO: if partially typed, we'd want to know too -local function fallible(t: number) - if t > 0 then - return true, t - end - return false, "must be positive" -end -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_ERROR_COUNT(1, result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 0); -} - -TEST_CASE_FIXTURE(ATSFixture, "var_typepack_any_gen_table") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -type Pair = {first: T, second: any} -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::Alias); - CHECK(module->ats.typeInfo[0].node == "type Pair = {first: T, second: any}"); -} - -TEST_CASE_FIXTURE(ATSFixture, "assign_uneq") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/B"] = R"( -local function greetings(name: string) - return "Hello, " .. name, nil -end - -local x, y = greetings("Dibri") -local x, y = greetings("Dibri"), nil -local x, y, z = greetings("Dibri") -- mismatch -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/B"); - LUAU_REQUIRE_ERROR_COUNT(1, result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/B"); - REQUIRE(module->ats.typeInfo.size() == 0); -} - -TEST_CASE_FIXTURE(ATSFixture, "var_typepack_any_gen") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( --- type Pair = (boolean, string, ...any) -> {T} -- type aliases with generics/pack do not seem to be processed? -type Pair = (boolean, T) -> ...any -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::Alias); - CHECK(module->ats.typeInfo[0].node == "type Pair = (boolean, T)->( ...any)"); -} - -TEST_CASE_FIXTURE(ATSFixture, "typeof_any_in_func") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - local function f() - local a: any = 1 - local b: typeof(a) = 1 - end -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 2); - CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot); - CHECK(module->ats.typeInfo[0].node == "local function f()\n local a: any = 1\n local b: typeof(a) = 1\n end"); -} - -TEST_CASE_FIXTURE(ATSFixture, "generic_types") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -local function foo(a: (...A) -> any, ...: A) - return a(...) -end - -local function addNumbers(num1, num2) - local result = num1 + num2 - return result -end - -foo(addNumbers) - )"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 3); - CHECK(module->ats.typeInfo[1].code == Pattern::FuncApp); - CHECK(module->ats.typeInfo[0].node == "local function foo(a: (...A)->( any),...: A)\n return a(...)\nend"); -} - -TEST_CASE_FIXTURE(ATSFixture, "no_annot") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -local character = script.Parent -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 0); -} - -TEST_CASE_FIXTURE(ATSFixture, "if_any") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -function f(x: any) -if not x then -x = { - y = math.random(0, 2^31-1), - left = nil, - right = nil -} -else - local expected = x * 5 -end -end -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); - CHECK( - module->ats.typeInfo[0].node == "function f(x: any)\nif not x then\nx = {\n y = math.random(0, 2^31-1),\n left = nil,\n right = " - "nil\n}\nelse\n local expected = x * 5\nend\nend" - ); -} - -TEST_CASE_FIXTURE(ATSFixture, "variadic_any") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - local function f(): (number, ...any) - return 1, 5 --catching this - end - - local x, y, z = f() -- not catching this any because no annot -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 2); - CHECK(module->ats.typeInfo[0].code == Pattern::FuncRet); - CHECK(module->ats.typeInfo[0].node == "local function f(): (number, ...any)\n return 1, 5\n end"); -} - -TEST_CASE_FIXTURE(ATSFixture, "type_alias_intersection") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - type XCoord = {x: number} - type YCoord = {y: any} - type Vector2 = XCoord & YCoord -- table type intersections do not get normalized - local vec2: Vector2 = {x = 1, y = 2} -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 3); - CHECK(module->ats.typeInfo[2].code == Pattern::VarAnnot); - CHECK(module->ats.typeInfo[2].node == "local vec2: Vector2 = {x = 1, y = 2}"); -} - -TEST_CASE_FIXTURE(ATSFixture, "var_func_arg") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - local function f(...: any) - end - local function f(x: number?, y, z: any) - end - function f(x: number?, y, z: any) - end - function f(...: any) - end -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 4); - CHECK(module->ats.typeInfo[0].code == Pattern::VarAny); - CHECK(module->ats.typeInfo[0].node == "local function f(...: any)\n end"); -} - -TEST_CASE_FIXTURE(ATSFixture, "var_func_apps") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - local function f(...: any) - end - f("string", 123) - f("string") -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 3); - CHECK(module->ats.typeInfo[0].code == Pattern::VarAny); - CHECK(module->ats.typeInfo[0].node == "local function f(...: any)\n end"); -} - - -TEST_CASE_FIXTURE(ATSFixture, "CannotExtendTable") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -local CAR_COLLISION_GROUP = "Car" - --- Set the car collision group -for _, descendant in carTemplate:GetDescendants() do - if descendant:IsA("BasePart") then - descendant.CollisionGroup = CAR_COLLISION_GROUP - end -end - -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_ERROR_COUNT(1, result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - if (FFlag::LuauSkipNoRefineDuringRefinement) - { - REQUIRE_EQ(module->ats.typeInfo.size(), 1); - CHECK_EQ(module->ats.typeInfo[0].code, Pattern::Assign); - CHECK_EQ(module->ats.typeInfo[0].node, "descendant.CollisionGroup = CAR_COLLISION_GROUP"); - } - else - REQUIRE(module->ats.typeInfo.size() == 0); -} - -TEST_CASE_FIXTURE(ATSFixture, "unknown_symbol") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -local function manageRace(raceContainer: Model) - RaceManager.new(raceContainer) -end - -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_ERROR_COUNT(2, result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 2); - CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); - CHECK(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend"); -} - -TEST_CASE_FIXTURE(ATSFixture, "racing_3_short") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - -local CollectionService = game:GetService("CollectionService") - -local RaceManager = require(script.RaceManager) - -local RACE_TAG = "Race" - -local function manageRace(raceContainer: Model) - RaceManager.new(raceContainer) -end - -local function initialize() - CollectionService:GetInstanceAddedSignal(RACE_TAG):Connect(manageRace) - - for _, raceContainer in CollectionService:GetTagged(RACE_TAG) do - manageRace(raceContainer) - end -end - -initialize() - -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_ERROR_COUNT(2, result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 5); - CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); - CHECK(module->ats.typeInfo[0].node == "local function manageRace(raceContainer: Model)\n RaceManager.new(raceContainer)\nend"); -} - -TEST_CASE_FIXTURE(ATSFixture, "racing_collision_2") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -local PhysicsService = game:GetService("PhysicsService") -local ReplicatedStorage = game:GetService("ReplicatedStorage") - -local safePlayerAdded = require(script.safePlayerAdded) - -local CAR_COLLISION_GROUP = "Car" -local CHARACTER_COLLISION_GROUP = "Character" - -local carTemplate = ReplicatedStorage.Car - -local function onCharacterAdded(character: Model) - -- Set the collision group for any parts that are added to the character - character.DescendantAdded:Connect(function(descendant) - if descendant:IsA("BasePart") then - descendant.CollisionGroup = CHARACTER_COLLISION_GROUP - end - end) - - -- Set the collision group for any parts currently in the character - for _, descendant in character:GetDescendants() do - if descendant:IsA("BasePart") then - descendant.CollisionGroup = CHARACTER_COLLISION_GROUP - end - end -end - -local function onPlayerAdded(player: Player) - player.CharacterAdded:Connect(onCharacterAdded) - - if player.Character then - onCharacterAdded(player.Character) - end -end - -local function initialize() - -- Setup collision groups - PhysicsService:RegisterCollisionGroup(CAR_COLLISION_GROUP) - PhysicsService:RegisterCollisionGroup(CHARACTER_COLLISION_GROUP) - - -- Stop the collision groups from colliding with each other - PhysicsService:CollisionGroupSetCollidable(CAR_COLLISION_GROUP, CAR_COLLISION_GROUP, false) - PhysicsService:CollisionGroupSetCollidable(CHARACTER_COLLISION_GROUP, CHARACTER_COLLISION_GROUP, false) - PhysicsService:CollisionGroupSetCollidable(CAR_COLLISION_GROUP, CHARACTER_COLLISION_GROUP, false) - - -- Set the car collision group - for _, descendant in carTemplate:GetDescendants() do - if descendant:IsA("BasePart") then - descendant.CollisionGroup = CAR_COLLISION_GROUP - end - end - - -- Set character collision groups for all players - safePlayerAdded(onPlayerAdded) -end - -initialize() - -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_ERROR_COUNT(3, result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - if (FFlag::LuauSkipNoRefineDuringRefinement) - REQUIRE_EQ(module->ats.typeInfo.size(), 12); - else - REQUIRE(module->ats.typeInfo.size() == 11); - CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); - if (FFlag::LuauStoreCSTData) - { - CHECK_EQ( - module->ats.typeInfo[0].node, - "local function onCharacterAdded(character: Model)\n\n character.DescendantAdded:Connect(function(descendant)\n if " - "descendant:IsA('BasePart') then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n end)\n\n\n for _, descendant in " - "character:GetDescendants() do\n if descendant:IsA('BasePart') then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n " - "end\nend" - ); - } - else - { - CHECK( - module->ats.typeInfo[0].node == - "local function onCharacterAdded(character: Model)\n\n character.DescendantAdded:Connect(function(descendant)\n if " - "descendant:IsA('BasePart')then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n end)\n\n\n for _, descendant in " - "character:GetDescendants()do\n if descendant:IsA('BasePart')then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n " - "end\nend" - ); - } -} - -TEST_CASE_FIXTURE(ATSFixture, "racing_spawning_1") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - // Previously we'd report an error because number <: 'a is not a - // supertype. - {FFlag::LuauTrackInteriorFreeTypesOnScope, true} - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -local CollectionService = game:GetService("CollectionService") -local Players = game:GetService("Players") - -local spawnCar = require(script.spawnCar) -local destroyPlayerCars = require(script.destroyPlayerCars) - -local spawnPromptTemplate = script.SpawnPrompt - -local KIOSK_TAG = "CarSpawnKiosk" - -local function setupKiosk(kiosk: Model) - local spawnLocation = kiosk:FindFirstChild("SpawnLocation") - assert(spawnLocation, `{kiosk:GetFullName()} has no SpawnLocation part`) - local promptPart = kiosk:FindFirstChild("Prompt") - assert(promptPart, `{kiosk:GetFullName()} has no Prompt part`) - - -- Hide the car spawn location - spawnLocation.Transparency = 1 - - -- Create a new prompt to spawn the car - local spawnPrompt = spawnPromptTemplate:Clone() - spawnPrompt.Parent = promptPart - - spawnPrompt.Triggered:Connect(function(player: Player) - -- Remove any existing cars the player has spawned - destroyPlayerCars(player) - -- Spawn a new car at the spawnLocation, owned by the player - spawnCar(spawnLocation.CFrame, player) - end) -end - -local function initialize() - -- Remove cars owned by players whenever they leave - Players.PlayerRemoving:Connect(destroyPlayerCars) - - -- Setup all car spawning kiosks - CollectionService:GetInstanceAddedSignal(KIOSK_TAG):Connect(setupKiosk) - - for _, kiosk in CollectionService:GetTagged(KIOSK_TAG) do - setupKiosk(kiosk) - end -end - -initialize() - -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_ERROR_COUNT(4, result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 7); - CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); - CHECK( - module->ats.typeInfo[0].node == - "local function setupKiosk(kiosk: Model)\n local spawnLocation = kiosk:FindFirstChild('SpawnLocation')\n assert(spawnLocation, " - "`{kiosk:GetFullName()} has no SpawnLocation part`)\n local promptPart = kiosk:FindFirstChild('Prompt')\n assert(promptPart, " - "`{kiosk:GetFullName()} has no Prompt part`)\n\n\n spawnLocation.Transparency = 1\n\n\n local spawnPrompt = " - "spawnPromptTemplate:Clone()\n spawnPrompt.Parent = promptPart\n\n spawnPrompt.Triggered:Connect(function(player: Player)\n\n " - "destroyPlayerCars(player)\n\n spawnCar(spawnLocation.CFrame, player)\n end)\nend" - ); -} - -TEST_CASE_FIXTURE(ATSFixture, "mutually_recursive_generic") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true} - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - --!strict - type T = { f: a, g: U } - type U = { h: a, i: T? } - local x: T = { f = 37, g = { h = 5, i = nil } } - x.g.i = x - local y: T = { f = "hi", g = { h = "lo", i = nil } } - y.g.i = y - )"; - - LUAU_REQUIRE_NO_ERRORS(frontend.check("game/Gui/Modules/A")); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 0); -} - -TEST_CASE_FIXTURE(ATSFixture, "explicit_pack") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -type Foo = (T...) -> () -- also want to see how these are used. -type Bar = Foo<(number, any)> -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::Alias); - CHECK(module->ats.typeInfo[0].node == "type Bar = Foo<(number, any)>"); -} - -TEST_CASE_FIXTURE(ATSFixture, "local_val") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -local a, b, c = 1 :: any -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::Casts); - CHECK(module->ats.typeInfo[0].node == "local a, b, c = 1 :: any"); -} - -TEST_CASE_FIXTURE(ATSFixture, "var_any_local") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( -local x = 2 -local x: any = 2, 3 -local x: any, y = 1, 2 -local x: number, y: any, z, h: nil = 1, nil -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 3); - CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot); - CHECK(module->ats.typeInfo[0].node == "local x: any = 2, 3"); -} - -TEST_CASE_FIXTURE(ATSFixture, "table_uses_any") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - local x: any = 0 - local y: number - local z = {x=x, y=y} -- not catching this -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot); - CHECK(module->ats.typeInfo[0].node == "local x: any = 0"); -} - -TEST_CASE_FIXTURE(ATSFixture, "typeof_any") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - local x: any = 0 - function some1(x: typeof(x)) - end -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 2); - CHECK(module->ats.typeInfo[1].code == Pattern::FuncArg); - CHECK(module->ats.typeInfo[0].node == "function some1(x: typeof(x))\n end"); -} - -TEST_CASE_FIXTURE(ATSFixture, "table_type_assigned") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - local x: { x: any?} = {x = 1} - local z: { x : any, y : number? } -- not catching this - z.x = "bigfatlongstring" - z.y = nil -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 2); - CHECK(module->ats.typeInfo[1].code == Pattern::Assign); - CHECK(module->ats.typeInfo[0].node == "local x: { x: any?} = {x = 1}"); -} - -TEST_CASE_FIXTURE(ATSFixture, "simple_func_wo_ret") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - function some(x: any) - end -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); - CHECK(module->ats.typeInfo[0].node == "function some(x: any)\n end"); -} - -TEST_CASE_FIXTURE(ATSFixture, "simple_func_w_ret") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - function other(y: number): any - return "gotcha!" - end -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::FuncRet); - CHECK(module->ats.typeInfo[0].node == "function other(y: number): any\n return 'gotcha!'\n end"); -} - -TEST_CASE_FIXTURE(ATSFixture, "nested_local") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - function cool(y: number): number - local g: any = "gratatataaa" - return y - end -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::VarAnnot); - CHECK(module->ats.typeInfo[0].node == "function cool(y: number): number\n local g: any = 'gratatataaa'\n return y\n end"); -} - -TEST_CASE_FIXTURE(ATSFixture, "generic_func") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - function reverse(a: {T}, b: any): {T} - return a - end -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 1); - CHECK(module->ats.typeInfo[0].code == Pattern::FuncArg); - CHECK(module->ats.typeInfo[0].node == "function reverse(a: {T}, b: any): {T}\n return a\n end"); -} - -TEST_CASE_FIXTURE(ATSFixture, "type_alias_any") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/Gui/Modules/A"] = R"( - type Clear = any - local z: Clear = "zip" -)"; - - CheckResult result1 = frontend.check("game/Gui/Modules/A"); - LUAU_REQUIRE_NO_ERRORS(result1); - - ModulePtr module = frontend.moduleResolver.getModule("game/Gui/Modules/A"); - - REQUIRE(module->ats.typeInfo.size() == 2); - CHECK(module->ats.typeInfo[0].code == Pattern::Alias); - CHECK(module->ats.typeInfo[0].node == "type Clear = any"); -} - -TEST_CASE_FIXTURE(ATSFixture, "multi_module_any") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/A"] = R"( - export type MyFunction = (number, string) -> (any) -)"; - - fileResolver.source["game/B"] = R"( - local MyFunc = require(script.Parent.A) - type Clear = any - local z: Clear = "zip" -)"; - - fileResolver.source["game/Gui/Modules/A"] = R"( - local Modules = game:GetService('Gui').Modules - local B = require(Modules.B) - return {hello = B.hello} -)"; - - CheckResult result = frontend.check("game/B"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - - ModulePtr module = frontend.moduleResolver.getModule("game/B"); - - REQUIRE(module->ats.typeInfo.size() == 2); - CHECK(module->ats.typeInfo[0].code == Pattern::Alias); - CHECK(module->ats.typeInfo[0].node == "type Clear = any"); -} - -TEST_CASE_FIXTURE(ATSFixture, "cast_on_cyclic_req") -{ - ScopedFastFlag sff[] = { - {FFlag::LuauSolverV2, true}, - {FFlag::StudioReportLuauAny2, true}, - }; - - fileResolver.source["game/A"] = R"( - local a = require(script.Parent.B) -- not resolving this module - export type MyFunction = (number, string) -> (any) -)"; - - fileResolver.source["game/B"] = R"( - local MyFunc = require(script.Parent.A) :: any - type Clear = any - local z: Clear = "zip" -)"; - - CheckResult result = frontend.check("game/B"); - LUAU_REQUIRE_ERROR_COUNT(0, result); - - ModulePtr module = frontend.moduleResolver.getModule("game/B"); - - REQUIRE(module->ats.typeInfo.size() == 3); - CHECK(module->ats.typeInfo[1].code == Pattern::Alias); - CHECK(module->ats.typeInfo[1].node == "type Clear = any"); -} - - -TEST_SUITE_END(); diff --git a/tests/AstJsonEncoder.test.cpp b/tests/AstJsonEncoder.test.cpp index 9e79a5f7..a4ae4fdc 100644 --- a/tests/AstJsonEncoder.test.cpp +++ b/tests/AstJsonEncoder.test.cpp @@ -12,6 +12,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauAstTypeGroup3) +LUAU_FASTFLAG(LuauFixFunctionWithAttributesStartLocation) struct JsonEncoderFixture { @@ -440,7 +441,9 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstAttr") AstStat* expr = expectParseStatement("@checked function a(b) return c end"); std::string_view expected = - R"({"type":"AstStatFunction","location":"0,9 - 0,35","name":{"type":"AstExprGlobal","location":"0,18 - 0,19","global":"a"},"func":{"type":"AstExprFunction","location":"0,9 - 0,35","attributes":[{"type":"AstAttr","location":"0,0 - 0,8","name":"checked"}],"generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"b","type":"AstLocal","location":"0,20 - 0,21"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,22 - 0,32","hasEnd":true,"body":[{"type":"AstStatReturn","location":"0,23 - 0,31","list":[{"type":"AstExprGlobal","location":"0,30 - 0,31","global":"c"}]}]},"functionDepth":1,"debugname":"a"}})"; + FFlag::LuauFixFunctionWithAttributesStartLocation + ? R"({"type":"AstStatFunction","location":"0,0 - 0,35","name":{"type":"AstExprGlobal","location":"0,18 - 0,19","global":"a"},"func":{"type":"AstExprFunction","location":"0,0 - 0,35","attributes":[{"type":"AstAttr","location":"0,0 - 0,8","name":"checked"}],"generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"b","type":"AstLocal","location":"0,20 - 0,21"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,22 - 0,32","hasEnd":true,"body":[{"type":"AstStatReturn","location":"0,23 - 0,31","list":[{"type":"AstExprGlobal","location":"0,30 - 0,31","global":"c"}]}]},"functionDepth":1,"debugname":"a"}})" + : R"({"type":"AstStatFunction","location":"0,9 - 0,35","name":{"type":"AstExprGlobal","location":"0,18 - 0,19","global":"a"},"func":{"type":"AstExprFunction","location":"0,9 - 0,35","attributes":[{"type":"AstAttr","location":"0,0 - 0,8","name":"checked"}],"generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"b","type":"AstLocal","location":"0,20 - 0,21"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,22 - 0,32","hasEnd":true,"body":[{"type":"AstStatReturn","location":"0,23 - 0,31","list":[{"type":"AstExprGlobal","location":"0,30 - 0,31","global":"c"}]}]},"functionDepth":1,"debugname":"a"}})"; CHECK(toJson(expr) == expected); } diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 3c07c088..e38ba4f9 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -23,6 +23,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete) LUAU_FASTFLAG(LuauAutocompleteUnionCopyPreviousSeen) LUAU_FASTFLAG(LuauUserTypeFunTypecheck) +LUAU_FASTFLAG(LuauTypeFunResultInAutocomplete) using namespace Luau; @@ -4496,4 +4497,27 @@ this@2 CHECK_EQ(ac.entryMap.count("thisShouldBeThere"), 0); } +TEST_CASE_FIXTURE(ACBuiltinsFixture, "type_function_eval_in_autocomplete") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + ScopedFastFlag luauTypeFunResultInAutocomplete{FFlag::LuauTypeFunResultInAutocomplete, true}; + + check(R"( +type function foo(x) + local tbl = types.newtable(nil, nil, nil) + tbl:setproperty(types.singleton("boolean"), x) + tbl:setproperty(types.singleton("number"), types.number) + return tbl +end + +local function test(a: foo) + return a.@1 +end + )"); + + auto ac = autocomplete('1'); + CHECK_EQ(ac.entryMap.count("boolean"), 1); + CHECK_EQ(ac.entryMap.count("number"), 1); +} + TEST_SUITE_END(); diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index 3ab85a1d..6dd69dba 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -372,7 +372,7 @@ LintResult Fixture::lint(const std::string& source, const std::optional& lintOptions) diff --git a/tests/FragmentAutocomplete.test.cpp b/tests/FragmentAutocomplete.test.cpp index fcb70573..82e2a257 100644 --- a/tests/FragmentAutocomplete.test.cpp +++ b/tests/FragmentAutocomplete.test.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include @@ -28,7 +29,6 @@ LUAU_FASTFLAG(LuauIncrementalAutocompleteCommentDetection) LUAU_FASTINT(LuauParseErrorLimit) LUAU_FASTFLAG(LuauCloneIncrementalModule) -LUAU_FASTFLAG(LuauIncrementalAutocompleteBugfixes) LUAU_FASTFLAG(LuauMixedModeDefFinderTraversesTypeOf) LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds) @@ -45,6 +45,7 @@ LUAU_FASTFLAG(LuauDoNotClonePersistentBindings) LUAU_FASTFLAG(LuauCloneReturnTypePack) LUAU_FASTFLAG(LuauIncrementalAutocompleteDemandBasedCloning) LUAU_FASTFLAG(LuauUserTypeFunTypecheck) +LUAU_FASTFLAG(LuauBetterScopeSelection) static std::optional nullCallback(std::string tag, std::optional ptr, std::optional contents) { @@ -75,7 +76,6 @@ struct FragmentAutocompleteFixtureImpl : BaseType static_assert(std::is_base_of_v, "BaseType must be a descendant of Fixture"); ScopedFastFlag luauAutocompleteRefactorsForIncrementalAutocomplete{FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete, true}; - ScopedFastFlag luauIncrementalAutocompleteBugfixes{FFlag::LuauIncrementalAutocompleteBugfixes, true}; ScopedFastFlag luauFreeTypesMustHaveBounds{FFlag::LuauFreeTypesMustHaveBounds, true}; ScopedFastFlag luauCloneIncrementalModule{FFlag::LuauCloneIncrementalModule, true}; ScopedFastFlag luauAllFreeTypesHaveScopes{FFlag::LuauAllFreeTypesHaveScopes, true}; @@ -86,6 +86,7 @@ struct FragmentAutocompleteFixtureImpl : BaseType ScopedFastFlag luauDoNotClonePersistentBindings{FFlag::LuauDoNotClonePersistentBindings, true}; ScopedFastFlag luauCloneReturnTypePack{FFlag::LuauCloneReturnTypePack, true}; ScopedFastFlag luauIncrementalAutocompleteDemandBasedCloning{FFlag::LuauIncrementalAutocompleteDemandBasedCloning, true}; + ScopedFastFlag luauBetterScopeSelection{FFlag::LuauBetterScopeSelection, true}; FragmentAutocompleteFixtureImpl() : BaseType(true) @@ -97,13 +98,27 @@ struct FragmentAutocompleteFixtureImpl : BaseType return this->check(source, getOptions()); } + ParseResult parseHelper(std::string document) + { + SourceModule& source = getSource(); + ParseOptions parseOptions; + parseOptions.captureComments = true; + ParseResult parseResult = Parser::parse(document.c_str(), document.length(), *source.names, *source.allocator, parseOptions); + return parseResult; + } + FragmentAutocompleteAncestryResult runAutocompleteVisitor(const std::string& source, const Position& cursorPos) { ParseResult p = this->tryParse(source); // We don't care about parsing incomplete asts REQUIRE(p.root); - return findAncestryForFragmentParse(p.root, cursorPos); + return findAncestryForFragmentParse(p.root, cursorPos, p.root); } + FragmentRegion getAutocompleteRegion(const std::string source, const Position& cursorPos) + { + ParseResult p = parseHelper(source); + return Luau::getFragmentRegion(p.root, cursorPos); + } std::optional parseFragment( const std::string& document, @@ -111,9 +126,10 @@ struct FragmentAutocompleteFixtureImpl : BaseType std::optional fragmentEndPosition = std::nullopt ) { + ParseResult p = parseHelper(document); ModulePtr module = this->getMainModule(getOptions().forAutocomplete); std::string_view srcString = document; - return Luau::parseFragment(module->root, module->names.get(), srcString, cursorPos, fragmentEndPosition); + return Luau::parseFragment(module->root, p.root, module->names.get(), srcString, cursorPos, fragmentEndPosition); } CheckResult checkOldSolver(const std::string& source) @@ -128,7 +144,8 @@ struct FragmentAutocompleteFixtureImpl : BaseType std::optional fragmentEndPosition = std::nullopt ) { - auto [_, result] = Luau::typecheckFragment(this->frontend, "MainModule", cursorPos, getOptions(), document, fragmentEndPosition); + ParseResult p = parseHelper(document); + auto [_, result] = Luau::typecheckFragment(this->frontend, "MainModule", cursorPos, getOptions(), document, fragmentEndPosition, p.root); return result; } @@ -140,8 +157,7 @@ struct FragmentAutocompleteFixtureImpl : BaseType { ParseOptions parseOptions; parseOptions.captureComments = true; - SourceModule source; - ParseResult parseResult = Parser::parse(document.c_str(), document.length(), *source.names, *source.allocator, parseOptions); + ParseResult parseResult = parseHelper(document); FrontendOptions options = getOptions(); FragmentContext context{document, parseResult, options, fragmentEndPosition}; return Luau::tryFragmentAutocomplete(this->frontend, "MainModule", cursorPos, context, nullCallback); @@ -178,7 +194,8 @@ struct FragmentAutocompleteFixtureImpl : BaseType std::optional fragmentEndPosition = std::nullopt ) { - return Luau::typecheckFragment(this->frontend, module, cursorPos, getOptions(), document, fragmentEndPosition); + ParseResult pr = parseHelper(document); + return Luau::typecheckFragment(this->frontend, module, cursorPos, getOptions(), document, fragmentEndPosition, pr.root); } FragmentAutocompleteStatusResult autocompleteFragmentForModule( @@ -190,12 +207,20 @@ struct FragmentAutocompleteFixtureImpl : BaseType { ParseOptions parseOptions; parseOptions.captureComments = true; - SourceModule source; - ParseResult parseResult = Parser::parse(document.c_str(), document.length(), *source.names, *source.allocator, parseOptions); + ParseResult parseResult = parseHelper(document); FrontendOptions options; FragmentContext context{document, parseResult, options, fragmentEndPosition}; return Luau::tryFragmentAutocomplete(this->frontend, module, cursorPos, context, nullCallback); } + + SourceModule& getSource() + { + source = std::make_unique(); + return *source; + } + +private: + std::unique_ptr source = std::make_unique(); }; struct FragmentAutocompleteFixture : FragmentAutocompleteFixtureImpl @@ -236,6 +261,638 @@ end } }; +TEST_SUITE_BEGIN("FragmentSelectionSpecTests"); + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "just_two_locals") +{ + auto region = getAutocompleteRegion( + R"( +local x = 4 +local y = 5 +)", + {2, 11} + ); + + CHECK_EQ(Location{{2, 0}, {2, 11}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + REQUIRE(region.nearestStatement); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "singleline_call") +{ + auto region = getAutocompleteRegion( + R"( +abc("foo") +)", + {1, 10} + ); + + CHECK_EQ(Location{{1, 0}, {1, 10}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + REQUIRE(region.nearestStatement); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "midway_multiline_call") +{ + auto region = getAutocompleteRegion( + R"( +abc( +"foo" +) +)", + {2, 4} + ); + + CHECK_EQ(Location{{1, 0}, {2, 4}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + REQUIRE(region.nearestStatement); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "end_multiline_call") +{ + auto region = getAutocompleteRegion( + R"( +abc( +"foo" +) +)", + {3, 1} + ); + + CHECK_EQ(Location{{1, 0}, {3, 1}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + REQUIRE(region.nearestStatement); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "midway_through_call") +{ + auto region = getAutocompleteRegion( + R"( +abc("foo") +)", + {1, 6} + ); + + CHECK_EQ(Location{{1, 0}, {1, 6}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + REQUIRE(region.nearestStatement); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "inside_incomplete_do") +{ + auto region = getAutocompleteRegion( + R"( +local x = 4 +do +)", + {2, 2} + ); + + CHECK_EQ(Location{{2, 2}, {2, 2}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "end_of_do") +{ + auto region = getAutocompleteRegion( + R"( +local x = 4 +do +end +)", + {3, 3} + ); + + CHECK_EQ(Location{{3, 3}, {3, 3}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "inside_do") +{ + auto region = getAutocompleteRegion( + R"( +local x = 4 +do + +end +)", + {3, 3} + ); + + CHECK_EQ(Location{{3, 3}, {3, 3}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "partial_statement_inside_do") +{ + auto region = getAutocompleteRegion( + R"( +local x = 4 +do + local x = +end +)", + {3, 13} + ); + + CHECK_EQ(Location{{3, 4}, {3, 13}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "partial_statement_after_do") +{ + auto region = getAutocompleteRegion( + R"( +local x = 4 +do + +end +local x = +)", + {5, 9} + ); + + CHECK_EQ(Location{{5, 0}, {5, 9}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "before_func") +{ + auto region = getAutocompleteRegion( + R"( +function f() +end +)", + {1, 0} + ); + CHECK_EQ(Location{{1, 0}, {1, 0}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "after_func_same_line") +{ + auto region = getAutocompleteRegion( + R"( +function f() +end +)", + {2, 3} + ); + CHECK_EQ(Location{{2, 3}, {2, 3}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "after_func_new_line") +{ + auto region = getAutocompleteRegion( + R"( +function f() +end + +)", + {3, 0} + ); + CHECK_EQ(Location{{3, 0}, {3, 0}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "while_writing_func") +{ + auto region = getAutocompleteRegion( + R"( +function f(arg1, +)", + {1, 17} + ); + CHECK_EQ(Location{{1, 0}, {1, 17}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "writing_func_annotation") +{ + auto region = getAutocompleteRegion( + R"( +function f(arg1 : T +)", + {1, 19} + ); + CHECK_EQ(Location{{1, 0}, {1, 19}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "writing_func_return") +{ + auto region = getAutocompleteRegion( + R"( +function f(arg1 : T) : +)", + {1, 22} + ); + CHECK_EQ(Location{{1, 0}, {1, 22}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "writing_func_return_pack") +{ + auto region = getAutocompleteRegion( + R"( +function f(arg1 : T) : T... +)", + {1, 27} + ); + CHECK_EQ(Location{{1, 0}, {1, 27}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "before_local_func") +{ + auto region = getAutocompleteRegion( + R"( +local function f() +end +)", + {1, 0} + ); + CHECK_EQ(Location{{1, 0}, {1, 0}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "after_local_func_same_line") +{ + auto region = getAutocompleteRegion( + R"( +local function f() +end +)", + {2, 3} + ); + CHECK_EQ(Location{{2, 3}, {2, 3}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "after_local_func_new_line") +{ + auto region = getAutocompleteRegion( + R"( +local function f() +end + +)", + {3, 0} + ); + CHECK_EQ(Location{{3, 0}, {3, 0}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "while_writing_local_func") +{ + auto region = getAutocompleteRegion( + R"( +local function f(arg1, +)", + {1, 22} + ); + CHECK_EQ(Location{{1, 0}, {1, 22}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "writing_local_func_annotation") +{ + auto region = getAutocompleteRegion( + R"( +local function f(arg1 : T +)", + {1, 25} + ); + CHECK_EQ(Location{{1, 0}, {1, 25}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "writing_local_func_return") +{ + auto region = getAutocompleteRegion( + R"( +local function f(arg1 : T) : +)", + {1, 28} + ); + CHECK_EQ(Location{{1, 0}, {1, 28}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "writing_local_func_return_pack") +{ + auto region = getAutocompleteRegion( + R"( +local function f(arg1 : T) : T... +)", + {1, 33} + ); + CHECK_EQ(Location{{1, 0}, {1, 33}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "single_line_local_and_annot") +{ + auto region = getAutocompleteRegion( + R"( +type Part = {x : number} +local part : Part = {x = 3}; pa +)", + {2, 32} + ); + CHECK_EQ(Location{{2, 29}, {2, 32}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "partial_while_in_condition") +{ + auto region = getAutocompleteRegion( + R"( +while t +)", + Position{1, 7} + ); + + CHECK_EQ(Location{{1, 0}, {1, 7}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "while_inside_condition_same_line") +{ + auto region = getAutocompleteRegion( + R"( +while true do +end +)", + Position{1, 13} + ); + + CHECK_EQ(Location{{1, 13}, {1, 13}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "partial_for_numeric_in_condition") +{ + auto region = getAutocompleteRegion( + R"( +for c = 1,3 +)", + Position{1, 11} + ); + + CHECK_EQ(Location{{1, 0}, {1, 11}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "partial_for_numeric_in_body") +{ + auto region = getAutocompleteRegion( + R"( +for c = 1,3 do +)", + Position{1, 14} + ); + + CHECK_EQ(Location{{1, 14}, {1, 14}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "partial_for_in_in_condition_1") +{ + auto region = getAutocompleteRegion( + R"( +for i,v in {1,2,3} +)", + Position{1, 18} + ); + + CHECK_EQ(Location{{1, 0}, {1, 18}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "partial_for_in_in_condition_2") +{ + auto region = getAutocompleteRegion( + R"( +for i,v in +)", + Position{1, 10} + ); + + CHECK_EQ(Location{{1, 0}, {1, 10}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "partial_for_in_in_condition_3") +{ + auto region = getAutocompleteRegion( + R"( +for i, +)", + Position{1, 6} + ); + + CHECK_EQ(Location{{1, 0}, {1, 6}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "partial_for_in_in_body") +{ + auto region = getAutocompleteRegion( + R"( +for i,v in {1,2,3} do +)", + Position{1, 21} + ); + + CHECK_EQ(Location{{1, 21}, {1, 21}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "if_partial") +{ + auto region = getAutocompleteRegion( + R"( +if +)", + Position{1, 3} + ); + CHECK_EQ(Location{{1, 0}, {1, 3}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "if_partial_in_condition_at") +{ + auto region = getAutocompleteRegion( + R"( +if true +)", + Position{1, 7} + ); + CHECK_EQ(Location{{1, 0}, {1, 7}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "if_partial_in_condition_after") +{ + auto region = getAutocompleteRegion( + R"( +if true +)", + Position{1, 8} + ); + CHECK_EQ(Location{{1, 8}, {1, 8}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "if_partial_after_condition") +{ + auto region = getAutocompleteRegion( + R"( +if true then +)", + Position{1, 12} + ); + CHECK_EQ(Location{{1, 12}, {1, 12}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "if_partial_new_line") +{ + auto region = getAutocompleteRegion( + R"( +if true then + +)", + Position{2, 0} + ); + CHECK_EQ(Location{{2, 0}, {2, 0}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "if_complete_inside_scope_line") +{ + auto region = getAutocompleteRegion( + R"( +if true then + local x = +end + +)", + Position{2, 13} + ); + CHECK_EQ(Location{{2, 4}, {2, 13}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "if_else_if") +{ + auto region = getAutocompleteRegion( + R"( +if true then +elseif +end + +)", + Position{2, 8} + ); + CHECK_EQ(Location{{2, 8}, {2, 8}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "if_else_if_no_end") +{ + auto region = getAutocompleteRegion( + R"( +if true then +elseif +)", + Position{2, 8} + ); + CHECK_EQ(Location{{2, 0}, {2, 8}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "if_else_if_after_then") +{ + auto region = getAutocompleteRegion( + R"( +if true then +elseif false then +end + +)", + Position{2, 17} + ); + CHECK_EQ(Location{{2, 17}, {2, 17}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "if_else_if_after_then_new_line") +{ + auto region = getAutocompleteRegion( + R"( +if true then +elseif false then + +end + +)", + Position{3, 0} + ); + CHECK_EQ(Location{{3, 0}, {3, 0}}, region.fragmentLocation); + REQUIRE(region.parentBlock); + CHECK(region.nearestStatement->as()); +} + + +TEST_SUITE_END(); + // NOLINTBEGIN(bugprone-unchecked-optional-access) TEST_SUITE_BEGIN("FragmentAutocompleteTraversalTests"); @@ -359,6 +1016,28 @@ TEST_SUITE_END(); TEST_SUITE_BEGIN("FragmentAutocompleteParserTests"); +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "empty_program_1") +{ + checkWithOptions(""); + ScopedFastInt sfi{FInt::LuauParseErrorLimit, 1}; + auto fragment = parseFragment("", Position(0, 39)); + REQUIRE(fragment); + CHECK(fragment->fragmentToParse == ""); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "empty_program_2") +{ + const std::string source = R"( + +)"; + checkWithOptions(source); + ScopedFastInt sfi{FInt::LuauParseErrorLimit, 1}; + auto fragment = parseFragment(source, Position(1, 39)); + REQUIRE(fragment); + CHECK(fragment->fragmentToParse == ""); +} + + TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "thrown_parse_error_leads_to_null_root") { checkWithOptions("type A = "); @@ -371,7 +1050,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "local_initializer") { ScopedFastFlag sff{FFlag::LuauSolverV2, true}; checkWithOptions("local a ="); - auto fragment = parseFragment("local a =", Position(0, 10)); + auto fragment = parseFragment("local a =", Position(0, 9)); REQUIRE(fragment.has_value()); CHECK_EQ("local a =", fragment->fragmentToParse); @@ -394,7 +1073,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "statement_in_empty_fragment_is_n Position(1, 0) ); REQUIRE(fragment.has_value()); - CHECK_EQ("\n", fragment->fragmentToParse); + CHECK_EQ("", fragment->fragmentToParse); CHECK_EQ(2, fragment->ancestry.size()); REQUIRE(fragment->root); CHECK_EQ(0, fragment->root->body.size); @@ -425,13 +1104,13 @@ local z = x + y REQUIRE(fragment.has_value()); - CHECK_EQ(Location{Position{2, 0}, Position{3, 15}}, fragment->root->location); + CHECK_EQ(Location{Position{3, 0}, Position{3, 15}}, fragment->root->location); - CHECK_EQ("local y = 5\nlocal z = x + y", fragment->fragmentToParse); + CHECK_EQ("local z = x + y", fragment->fragmentToParse); CHECK_EQ(5, fragment->ancestry.size()); REQUIRE(fragment->root); - CHECK_EQ(2, fragment->root->body.size); - auto stat = fragment->root->body.data[1]->as(); + CHECK_EQ(1, fragment->root->body.size); + auto stat = fragment->root->body.data[0]->as(); REQUIRE(stat); CHECK_EQ(1, stat->vars.size); CHECK_EQ(1, stat->values.size); @@ -504,7 +1183,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_in_correct_scope") local myInnerLocal = 1 end - )"); +)"); auto fragment = parseFragment( R"( @@ -513,13 +1192,13 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_in_correct_scope") local myInnerLocal = 1 end - )", +)", Position{6, 0} ); REQUIRE(fragment.has_value()); - CHECK_EQ("\n ", fragment->fragmentToParse); + CHECK_EQ("", fragment->fragmentToParse); } TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "can_parse_single_line_fragment_override") @@ -540,8 +1219,9 @@ abc("bar") REQUIRE(callFragment.has_value()); - CHECK_EQ("function abc(foo: string) end\nabc(\"foo\")", callFragment->fragmentToParse); - CHECK(callFragment->nearestStatement->is()); + CHECK_EQ("abc(\"foo\")", callFragment->fragmentToParse); + CHECK(callFragment->nearestStatement); + CHECK(callFragment->nearestStatement->is()); CHECK_GE(callFragment->ancestry.size(), 2); @@ -567,8 +1247,9 @@ abc("bar") REQUIRE(stringFragment.has_value()); - CHECK_EQ("function abc(foo: string) end\nabc(\"foo\")", stringFragment->fragmentToParse); - CHECK(stringFragment->nearestStatement->is()); + CHECK_EQ("abc(\"foo\")", stringFragment->fragmentToParse); + CHECK(stringFragment->nearestStatement); + CHECK(stringFragment->nearestStatement->is()); CHECK_GE(stringFragment->ancestry.size(), 1); @@ -604,8 +1285,9 @@ abc("bar") REQUIRE(fragment.has_value()); - CHECK_EQ("function abc(foo: string) end\nabc(\n\"foo\"\n)", fragment->fragmentToParse); - CHECK(fragment->nearestStatement->is()); + CHECK_EQ("abc(\n\"foo\"\n)", fragment->fragmentToParse); + CHECK(fragment->nearestStatement); + CHECK(fragment->nearestStatement->is()); CHECK_GE(fragment->ancestry.size(), 2); @@ -969,14 +1651,14 @@ tbl.abc. TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "multiple_functions_complex") { const std::string text = R"( local function f1(a1) - local l1 = 1;" - g1 = 1;" + local l1 = 1; + g1 = 1; end local function f2(a2) local l2 = 1; g2 = 1; -end +end )"; autocompleteFragmentInBothSolvers( @@ -1064,8 +1746,7 @@ end REQUIRE(fragment.result); auto strings = fragment.result->acResults.entryMap; CHECK(strings.count("f1") != 0); - // FIXME: RIDE-11123: This should be zero counts of `a1`. - CHECK(strings.count("a1") != 0); + CHECK(strings.count("a1") == 0); CHECK(strings.count("l1") == 0); CHECK(strings.count("g1") != 0); CHECK(strings.count("f2") == 0); @@ -1107,15 +1788,14 @@ end CHECK(strings.count("l1") == 0); CHECK(strings.count("g1") != 0); CHECK(strings.count("f2") != 0); - // FIXME: RIDE-11123: This should be zero counts of `a2`. - CHECK(strings.count("a2") != 0); + CHECK(strings.count("a2") == 0); CHECK(strings.count("l2") == 0); CHECK(strings.count("g2") != 0); } ); } -TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "inline_autocomplete_picks_the_right_scope") +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "inline_autocomplete_picks_the_right_scope_1") { const std::string source = R"( type Table = { a: number, b: number } @@ -1149,6 +1829,41 @@ end ); } +TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "inline_autocomplete_picks_the_right_scope_2") +{ + const std::string source = R"( +type Table = { a: number, b: number } +do + type Table = { x: string, y: string } +end +)"; + + const std::string updated = R"( +type Table = { a: number, b: number } +do + type Table = { x: string, y: string } +end +local a : T +)"; + + autocompleteFragmentInBothSolvers( + source, + updated, + Position{5, 11}, + [](FragmentAutocompleteStatusResult& fragment) + { + REQUIRE(fragment.result); + LUAU_ASSERT(fragment.result->freshScope); + REQUIRE(fragment.result->acResults.entryMap.count("Table")); + REQUIRE(fragment.result->acResults.entryMap["Table"].type); + const TableType* tv = get(follow(*fragment.result->acResults.entryMap["Table"].type)); + REQUIRE(tv); + CHECK(tv->props.count("a")); + CHECK(tv->props.count("b")); + } + ); +} + TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "nested_recursive_function") { const std::string source = R"( @@ -1194,7 +1909,7 @@ TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "empty_program") autocompleteFragmentInBothSolvers( "", "", - Position{0, 1}, + Position{0, 0}, [](FragmentAutocompleteStatusResult& frag) { REQUIRE(frag.result); @@ -1762,10 +2477,33 @@ end ); } -TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "bad_range") +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "bad_range_1") { const std::string source = R"( -l +local t = 1 +)"; + const std::string updated = R"( +t +)"; + + autocompleteFragmentInBothSolvers( + source, + updated, + Position{2, 1}, + [](FragmentAutocompleteStatusResult& frag) + { + REQUIRE(frag.result); + auto opt = linearSearchForBinding(frag.result->freshScope, "t"); + REQUIRE(opt); + CHECK_EQ("number", toString(*opt)); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "bad_range_2") +{ + const std::string source = R"( +local t = 1 )"; const std::string updated = R"( local t = 1 @@ -1786,6 +2524,33 @@ t ); } +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "bad_range_3") +{ + // This test makes less sense since we don't have an updated check that + // includes l + // instead this will recommend nothing useful because `local t` hasn't + // been typechecked in the fresh module + const std::string source = R"( +l +)"; + const std::string updated = R"( +local t = 1 +l +)"; + + autocompleteFragmentInBothSolvers( + source, + updated, + Position{2, 1}, + [](FragmentAutocompleteStatusResult& frag) + { + CHECK(frag.status == FragmentAutocompleteStatus::Success); + REQUIRE(frag.result); + } + ); +} + + TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "do_not_recommend_results_in_multiline_comment") { ScopedFastFlag sff[] = {{FFlag::LuauIncrementalAutocompleteCommentDetection, true}, {FFlag::LuauBetterCursorInCommentDetection, true}}; @@ -2081,7 +2846,7 @@ type a = typeof({}) )"; const std::string dest = R"( type a = typeof({}) -l +type a = typeof({}) )"; // Re-parsing and typechecking a type alias in the fragment that was defined in the base module will assert in ConstraintGenerator::checkAliases @@ -2089,7 +2854,7 @@ l autocompleteFragmentInBothSolvers( source, dest, - Position{2, 2}, + Position{2, 20}, [](FragmentAutocompleteStatusResult& frag) { REQUIRE(frag.result); @@ -2382,4 +3147,127 @@ end CHECK(result.status != FragmentAutocompleteStatus::InternalIce); } +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "for_loop_recommends") +{ + const std::string source = R"( +local testArr: {{a: number, b: number}} = { +{a = 1, b = 2}, +{a = 2, b = 4}, +} + +for _, v in testArr do + +end +)"; + + const std::string dest = R"( +local testArr: {{a: number, b: number}} = { +{a = 1, b = 2}, +{a = 2, b = 4}, +} + +for _, v in testArr do + print(v. +end +)"; + + autocompleteFragmentInBothSolvers( + source, + dest, + Position{7, 12}, + [](FragmentAutocompleteStatusResult& result) + { + CHECK(result.status != FragmentAutocompleteStatus::InternalIce); + CHECK(result.result); + CHECK(!result.result->acResults.entryMap.empty()); + CHECK(result.result->acResults.entryMap.count("a")); + CHECK(result.result->acResults.entryMap.count("b")); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "for_loop_recommends") +{ + const std::string source = R"( +local testArr: {string} = { +"a", +"b", +} + +for _, v in testArr do + +end +)"; + + const std::string dest = R"( +local testArr: {string} = { +"a", +"b", +} + +for _, v in testArr do + print(v:) +end +)"; + + autocompleteFragmentInBothSolvers( + source, + dest, + Position{7, 12}, + [](FragmentAutocompleteStatusResult& result) + { + CHECK(result.status != FragmentAutocompleteStatus::InternalIce); + CHECK(result.result); + CHECK(!result.result->acResults.entryMap.empty()); + CHECK(result.result->acResults.entryMap.count("upper")); + CHECK(result.result->acResults.entryMap.count("sub")); + } + ); +} + +TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "expr_function") +{ + const std::string source = R"( +local t = {} +type Input = {x : string} +function t.Do(fn : (Input) -> ()) + if t.x == "a" then + return + end +end + +t.Do(function (f) + f +end) +)"; + + const std::string dest = R"( +local t = {} +type Input = {x : string} +function t.Do(fn : (Input) -> ()) + if t.x == "a" then + return + end +end + +t.Do(function (f) + f. +end) +)"; + + autocompleteFragmentInBothSolvers( + source, + dest, + Position{10, 6}, + [](FragmentAutocompleteStatusResult& status) + { + CHECK(FragmentAutocompleteStatus::Success == status.status); + REQUIRE(status.result); + CHECK(!status.result->acResults.entryMap.empty()); + CHECK(status.result->acResults.entryMap.count("x")); + } + ); +} + + TEST_SUITE_END(); diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index 9162ccf3..816c6f93 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -9,6 +9,7 @@ LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LintRedundantNativeAttribute); +LUAU_FASTFLAG(LuauDeprecatedAttribute); using namespace Luau; @@ -1600,6 +1601,326 @@ setfenv(h :: any, {}) CHECK_EQ(result.warnings[3].location.begin.line + 1, 11); } +static void checkDeprecatedWarning(const Luau::LintWarning& warning, const Luau::Position& begin, const Luau::Position& end, const char* msg) +{ + CHECK_EQ(warning.code, LintWarning::Code_DeprecatedApi); + CHECK_EQ(warning.location, Location(begin, end)); + CHECK_EQ(warning.text, msg); +} + +TEST_CASE_FIXTURE(Fixture, "DeprecatedAttribute") +{ + ScopedFastFlag sff[] = {{FFlag::LuauDeprecatedAttribute, true}, {FFlag::LuauSolverV2, true}}; + + // @deprecated works on local functions + { + LintResult result = lint(R"( +@deprecated +local function testfun(x) + return x + 1 +end + +testfun(1) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated"); + } + + // @deprecated works on globals functions + { + LintResult result = lint(R"( +@deprecated +function testfun(x) + return x + 1 +end + +testfun(1) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated"); + } + + // @deprecated works on fully typed functions + { + LintResult result = lint(R"( +@deprecated +local function testfun(x:number):number + return x + 1 +end + +if math.random(2) == 2 then + testfun(1) +end +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(7, 4), Position(7, 11), "Function 'testfun' is deprecated"); + } + + // @deprecated works on functions without an explicit return type + { + LintResult result = lint(R"( +@deprecated +local function testfun(x:number) + return x + 1 +end + +g(testfun) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(6, 2), Position(6, 9), "Function 'testfun' is deprecated"); + } + + // @deprecated works on functions without an explicit argument type + { + LintResult result = lint(R"( +@deprecated +local function testfun(x):number + if x == 1 then + return x + else + return 1 + testfun(x - 1) + end +end + +testfun(1) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(10, 0), Position(10, 7), "Function 'testfun' is deprecated"); + } + + // @deprecated works on inner functions + { + LintResult result = lint(R"( +function flipFlop() + local state = false + + @deprecated + local function invert() + state = !state + return state + end + + return invert +end + +f = flipFlop() +assert(f() == true) +)"); + + REQUIRE(2 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(10, 11), Position(10, 17), "Function 'invert' is deprecated"); + checkDeprecatedWarning(result.warnings[1], Position(14, 7), Position(14, 8), "Function 'f' is deprecated"); + } + + // @deprecated does not automatically apply to inner functions + { + LintResult result = lint(R"( +@deprecated +function flipFlop() + local state = false + + local function invert() + state = !state + return state + end + + return invert +end + +f = flipFlop() +assert(f() == true) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(13, 4), Position(13, 12), "Function 'flipFlop' is deprecated"); + } + + // @deprecated works correctly if deprecated function is shadowed + { + LintResult result = lint(R"( +@deprecated +local function doTheThing() + print("doing") +end + +doTheThing() + +local function shadow() + local function doTheThing() + print("doing!") + end + + doTheThing() +end + +shadow() +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 10), "Function 'doTheThing' is deprecated"); + } + + // @deprecated does not issue warnings if a deprecated function uses itself + { + LintResult result = lint(R"( +@deprecated +function fibonacci(n) + if n == 0 then + return 0 + elseif n == 1 then + return 1 + else + return fibonacci(n - 1) + fibonacci(n - 2) + end +end + +fibonacci(5) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(12, 0), Position(12, 9), "Function 'fibonacci' is deprecated"); + } + + // @deprecated works for mutually recursive functions + { + LintResult result = lint(R"( +@deprecated +function odd(x) + if x == 0 then + return false + else + return even(x - 1) + end +end + +@deprecated +function even(x) + if x == 0 then + return true + else + return odd(x - 1) + end +end + +assert(odd(1) == true) +assert(even(0) == true) +)"); + + REQUIRE(4 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(6, 15), Position(6, 19), "Function 'even' is deprecated"); + checkDeprecatedWarning(result.warnings[1], Position(15, 15), Position(15, 18), "Function 'odd' is deprecated"); + checkDeprecatedWarning(result.warnings[2], Position(19, 7), Position(19, 10), "Function 'odd' is deprecated"); + checkDeprecatedWarning(result.warnings[3], Position(20, 7), Position(20, 11), "Function 'even' is deprecated"); + } + + // @deprecated works for methods with a literal class name + { + LintResult result = lint(R"( +Account = { balance=0 } + +@deprecated +function Account:deposit(v) + self.balance = self.balance + v +end + +Account:deposit(200.00) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(8, 0), Position(8, 15), "Member 'Account.deposit' is deprecated"); + } + + // @deprecated works for methods with a compound expression class name + { + LintResult result = lint(R"( +Account = { balance=0 } + +function getAccount() + return Account +end + +@deprecated +function Account:deposit (v) + self.balance = self.balance + v +end + +(getAccount()):deposit(200.00) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(12, 0), Position(12, 22), "Member 'deposit' is deprecated"); + } +} + +TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeFunctionDeclaration") +{ + ScopedFastFlag sff[] = {{FFlag::LuauDeprecatedAttribute, true}, {FFlag::LuauSolverV2, true}}; + + // @deprecated works on function type declarations + + loadDefinition(R"( +@deprecated declare function bar(x: number): string +)"); + + LintResult result = lint(R"( +bar(2) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(1, 0), Position(1, 3), "Function 'bar' is deprecated"); +} + +TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeTableDeclaration") +{ + ScopedFastFlag sff[] = {{FFlag::LuauDeprecatedAttribute, true}, {FFlag::LuauSolverV2, true}}; + + // @deprecated works on table type declarations + + loadDefinition(R"( +declare Hooty : { + tooty : @deprecated @checked (number) -> number +} +)"); + + LintResult result = lint(R"( +print(Hooty:tooty(2.0)) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(1, 6), Position(1, 17), "Member 'Hooty.tooty' is deprecated"); +} + +TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeMethodDeclaration") +{ + ScopedFastFlag sff[] = {{FFlag::LuauDeprecatedAttribute, true}, {FFlag::LuauSolverV2, true}}; + + // @deprecated works on table type declarations + + loadDefinition(R"( +declare class Foo + @deprecated + function bar(self, value: number) : number +end + +declare Foo: { + new: () -> Foo +} +)"); + + LintResult result = lint(R"( +local foo = Foo.new() +print(foo:bar(2.0)) +)"); + + REQUIRE(1 == result.warnings.size()); + checkDeprecatedWarning(result.warnings[0], Position(2, 6), Position(2, 13), "Member 'bar' is deprecated"); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperations") { LintResult result = lint(R"( diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index ce45c57b..4c00a344 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -10,9 +10,15 @@ #include "Luau/Normalize.h" #include "Luau/BuiltinDefinitions.h" +LUAU_FASTFLAG(LuauNormalizeNegatedErrorToAnError) +LUAU_FASTFLAG(LuauNormalizeIntersectErrorToAnError) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTINT(LuauTypeInferRecursionLimit) -LUAU_FASTFLAG(LuauNormalizeNegationFix) +LUAU_FASTINT(LuauNormalizeIntersectionLimit) +LUAU_FASTINT(LuauNormalizeUnionLimit) +LUAU_FASTFLAG(LuauNormalizeLimitFunctionSet) +LUAU_FASTFLAG(LuauSubtypingStopAtNormFail) + using namespace Luau; namespace @@ -593,6 +599,25 @@ TEST_CASE_FIXTURE(NormalizeFixture, "intersect_truthy_expressed_as_intersection" )"))); } +TEST_CASE_FIXTURE(NormalizeFixture, "intersect_error") +{ + ScopedFastFlag luauNormalizeIntersectErrorToAnError{FFlag::LuauNormalizeIntersectErrorToAnError, true}; + + std::shared_ptr norm = toNormalizedType(R"(string & AAA)", 1); + REQUIRE(norm); + CHECK("*error-type*" == toString(normalizer.typeFromNormal(*norm))); +} + +TEST_CASE_FIXTURE(NormalizeFixture, "intersect_not_error") +{ + ScopedFastFlag luauNormalizeIntersectErrorToAnError{FFlag::LuauNormalizeIntersectErrorToAnError, true}; + ScopedFastFlag luauNormalizeNegatedErrorToAnError{FFlag::LuauNormalizeNegatedErrorToAnError, true}; + + std::shared_ptr norm = toNormalizedType(R"(string & Not<)", 1); + REQUIRE(norm); + CHECK("*error-type*" == toString(normalizer.typeFromNormal(*norm))); +} + TEST_CASE_FIXTURE(NormalizeFixture, "union_of_union") { CHECK(R"("alpha" | "beta" | "gamma")" == toString(normal(R"( @@ -1032,7 +1057,6 @@ TEST_CASE_FIXTURE(NormalizeFixture, "free_type_and_not_truthy") { ScopedFastFlag sff[] = { {FFlag::LuauSolverV2, true}, // Only because it affects the stringification of free types - {FFlag::LuauNormalizeNegationFix, true}, }; TypeId freeTy = arena.freshType(builtinTypes, &globalScope); @@ -1153,4 +1177,38 @@ end )"); } +TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_limit_function_intersection_complexity") +{ + ScopedFastInt luauNormalizeIntersectionLimit{FInt::LuauNormalizeIntersectionLimit, 50}; + ScopedFastInt luauNormalizeUnionLimit{FInt::LuauNormalizeUnionLimit, 20}; + ScopedFastFlag luauNormalizeLimitFunctionSet{FFlag::LuauNormalizeLimitFunctionSet, true}; + ScopedFastFlag luauSubtypingStopAtNormFail{FFlag::LuauSubtypingStopAtNormFail, true}; + + CheckResult result = check(R"( +function _(_).readu32(l0) +return ({[_(_(_))]=_,[_(if _ then _)]=_,n0=_,})[_],nil +end +_(_)[_(n32)] %= _(_(_)) + )"); + + LUAU_REQUIRE_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_propagate_normalization_failures") +{ + ScopedFastInt luauNormalizeIntersectionLimit{FInt::LuauNormalizeIntersectionLimit, 50}; + ScopedFastInt luauNormalizeUnionLimit{FInt::LuauNormalizeUnionLimit, 20}; + ScopedFastFlag luauNormalizeLimitFunctionSet{FFlag::LuauNormalizeLimitFunctionSet, true}; + ScopedFastFlag luauSubtypingStopAtNormFail{FFlag::LuauSubtypingStopAtNormFail, true}; + + CheckResult result = check(R"( +function _(_,"").readu32(l0) +return ({[_(_(_))]=_,[_(if _ then _,_())]=_,[""]=_,})[_],nil +end +_().readu32 %= _(_(_(_),_)) + )"); + + LUAU_REQUIRE_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index 2f4e7be2..6ca13aed 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -18,12 +18,17 @@ LUAU_FASTINT(LuauParseErrorLimit) LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauAllowComplexTypesInGenericParams) LUAU_FASTFLAG(LuauErrorRecoveryForTableTypes) -LUAU_FASTFLAG(LuauFixFunctionNameStartPosition) LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) LUAU_FASTFLAG(LuauAstTypeGroup3) LUAU_FASTFLAG(LuauFixDoBlockEndLocation) -LUAU_FASTFLAG(LuauParseOptionalAsNode) +LUAU_FASTFLAG(LuauParseOptionalAsNode2) +LUAU_FASTFLAG(LuauParseStringIndexer) +LUAU_FASTFLAG(LuauFixFunctionWithAttributesStartLocation) +LUAU_DYNAMIC_FASTFLAG(DebugLuauReportReturnTypeVariadicWithTypeSuffix) + +// Clip with DebugLuauReportReturnTypeVariadicWithTypeSuffix +extern bool luau_telemetry_parsed_return_type_variadic_with_type_suffix; namespace { @@ -2541,6 +2546,40 @@ TEST_CASE_FIXTURE(Fixture, "do_block_end_location_is_after_end_token") CHECK_EQ(block->location, Location{{1, 8}, {3, 11}}); } +TEST_CASE_FIXTURE(Fixture, "function_start_locations_are_before_attributes") +{ + ScopedFastFlag _{FFlag::LuauFixFunctionWithAttributesStartLocation, true}; + + AstStatBlock* stat = parse(R"( + @native + function globalFunction() + end + + @native + local function localFunction() + end + + local _ = @native function() + end + )"); + REQUIRE(stat); + REQUIRE_EQ(3, stat->body.size); + + auto globalFunction = stat->body.data[0]->as(); + REQUIRE(globalFunction); + CHECK_EQ(globalFunction->location, Location({1, 8}, {3, 11})); + + auto localFunction = stat->body.data[1]->as(); + REQUIRE(localFunction); + CHECK_EQ(localFunction->location, Location({5, 8}, {7, 11})); + + auto localVariable = stat->body.data[2]->as(); + REQUIRE(localVariable); + REQUIRE_EQ(localVariable->values.size, 1); + auto anonymousFunction = localVariable->values.data[0]->as(); + CHECK_EQ(anonymousFunction->location, Location({9, 18}, {10, 11})); +} + TEST_SUITE_END(); TEST_SUITE_BEGIN("ParseErrorRecovery"); @@ -3819,7 +3858,7 @@ TEST_CASE_FIXTURE(Fixture, "grouped_function_type") } else CHECK(unionTy->types.data[0]->is()); // () -> () - if (FFlag::LuauParseOptionalAsNode) + if (FFlag::LuauParseOptionalAsNode2) CHECK(unionTy->types.data[1]->is()); // ? else CHECK(unionTy->types.data[1]->is()); // nil @@ -3880,7 +3919,6 @@ TEST_CASE_FIXTURE(Fixture, "recover_from_bad_table_type") TEST_CASE_FIXTURE(Fixture, "function_name_has_correct_start_location") { - ScopedFastFlag _{FFlag::LuauFixFunctionNameStartPosition, true}; AstStatBlock* block = parse(R"( function simple() end @@ -3929,6 +3967,8 @@ TEST_CASE_FIXTURE(Fixture, "stat_end_includes_semicolon_position") TEST_CASE_FIXTURE(Fixture, "parsing_type_suffix_for_return_type_with_variadic") { + ScopedFastFlag sff{DFFlag::DebugLuauReportReturnTypeVariadicWithTypeSuffix, true}; + ParseResult result = tryParse(R"( function foo(): (string, ...number) | boolean end @@ -3936,7 +3976,13 @@ TEST_CASE_FIXTURE(Fixture, "parsing_type_suffix_for_return_type_with_variadic") // TODO(CLI-140667): this should produce a ParseError in future when we fix the invalid syntax CHECK(result.errors.size() == 0); + CHECK_EQ(luau_telemetry_parsed_return_type_variadic_with_type_suffix, true); } +TEST_CASE_FIXTURE(Fixture, "parsing_string_union_indexers") +{ + ScopedFastFlag _{FFlag::LuauParseStringIndexer, true}; + parse(R"(type foo = { ["bar" | "baz"]: number })"); +} TEST_SUITE_END(); diff --git a/tests/ScopedFlags.h b/tests/ScopedFlags.h index beb3cc06..b8b04c83 100644 --- a/tests/ScopedFlags.h +++ b/tests/ScopedFlags.h @@ -3,7 +3,7 @@ #include "Luau/Common.h" -#include +#include template struct [[nodiscard]] ScopedFValue @@ -49,4 +49,4 @@ public: }; using ScopedFastFlag = ScopedFValue; -using ScopedFastInt = ScopedFValue; +using ScopedFastInt = ScopedFValue; \ No newline at end of file diff --git a/tests/Transpiler.test.cpp b/tests/Transpiler.test.cpp index ba229a1e..58fb65ea 100644 --- a/tests/Transpiler.test.cpp +++ b/tests/Transpiler.test.cpp @@ -12,9 +12,11 @@ using namespace Luau; -LUAU_FASTFLAG(LuauStoreCSTData) +LUAU_FASTFLAG(LuauStoreCSTData2) LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon) LUAU_FASTFLAG(LuauAstTypeGroup3); +LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType) +LUAU_FASTFLAG(LuauParseOptionalAsNode2) TEST_SUITE_BEGIN("TranspilerTests"); @@ -48,7 +50,7 @@ TEST_CASE("string_literals_containing_utf8") TEST_CASE("if_stmt_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string one = R"( if This then Once() end)"; CHECK_EQ(one, transpile(one).code); @@ -97,7 +99,7 @@ TEST_CASE("elseif_chains_indent_sensibly") TEST_CASE("strips_type_annotations") { const std::string code = R"( local s: string= 'hello there' )"; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { const std::string expected = R"( local s = 'hello there' )"; CHECK_EQ(expected, transpile(code).code); @@ -112,7 +114,7 @@ TEST_CASE("strips_type_annotations") TEST_CASE("strips_type_assertion_expressions") { const std::string code = R"( local s= some_function() :: any+ something_else() :: number )"; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { const std::string expected = R"( local s= some_function() + something_else() )"; CHECK_EQ(expected, transpile(code).code); @@ -148,7 +150,7 @@ TEST_CASE("for_loop") TEST_CASE("for_loop_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string one = R"( for index = 1, 10 do call(index) end )"; CHECK_EQ(one, transpile(one).code); @@ -173,7 +175,7 @@ TEST_CASE("for_in_loop") TEST_CASE("for_in_loop_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string one = R"( for k, v in ipairs(x) do end )"; CHECK_EQ(one, transpile(one).code); @@ -198,7 +200,7 @@ TEST_CASE("while_loop") TEST_CASE("while_loop_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string one = R"( while f(x) do print() end )"; CHECK_EQ(one, transpile(one).code); @@ -220,7 +222,7 @@ TEST_CASE("repeat_until_loop") TEST_CASE("repeat_until_loop_condition_on_new_line") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( repeat print() @@ -252,7 +254,7 @@ TEST_CASE("local_assignment") TEST_CASE("local_assignment_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string one = R"( local x = 1 )"; CHECK_EQ(one, transpile(one).code); @@ -286,7 +288,7 @@ TEST_CASE("local_function") TEST_CASE("local_function_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string one = R"( local function p(o, m, ...) end )"; CHECK_EQ(one, transpile(one).code); @@ -305,7 +307,7 @@ TEST_CASE("function") TEST_CASE("function_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string two = R"( function p(o, m, ...) end )"; CHECK_EQ(two, transpile(two).code); @@ -333,7 +335,7 @@ TEST_CASE("function_spaces_around_tokens") TEST_CASE("function_with_types_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( function p(o: string, m: number, ...: any): string end )"; CHECK_EQ(code, transpile(code, {}, true).code); @@ -394,7 +396,7 @@ TEST_CASE("function_with_types_spaces_around_tokens") TEST_CASE("returns_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string one = R"( return 1 )"; CHECK_EQ(one, transpile(one).code); @@ -407,7 +409,7 @@ TEST_CASE("returns_spaces_around_tokens") TEST_CASE_FIXTURE(Fixture, "type_alias_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type Foo = string )"; CHECK_EQ(code, transpile(code, {}, true).code); @@ -456,7 +458,7 @@ TEST_CASE_FIXTURE(Fixture, "type_alias_spaces_around_tokens") TEST_CASE_FIXTURE(Fixture, "type_alias_with_defaults_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type Foo = string )"; CHECK_EQ(code, transpile(code, {}, true).code); @@ -517,7 +519,7 @@ TEST_CASE("table_literal_closing_brace_at_correct_position") TEST_CASE("table_literal_with_semicolon_separators") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local t = { x = 1; y = 2 } )"; @@ -527,7 +529,7 @@ TEST_CASE("table_literal_with_semicolon_separators") TEST_CASE("table_literal_with_trailing_separators") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local t = { x = 1, y = 2, } )"; @@ -537,7 +539,7 @@ TEST_CASE("table_literal_with_trailing_separators") TEST_CASE("table_literal_with_spaces_around_separator") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local t = { x = 1 , y = 2 } )"; @@ -547,7 +549,7 @@ TEST_CASE("table_literal_with_spaces_around_separator") TEST_CASE("table_literal_with_spaces_around_equals") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local t = { x = 1 } )"; @@ -557,7 +559,7 @@ TEST_CASE("table_literal_with_spaces_around_equals") TEST_CASE("table_literal_multiline_with_indexers") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local t = { ["my first value"] = "x"; @@ -585,7 +587,7 @@ TEST_CASE("spaces_between_keywords_even_if_it_pushes_the_line_estimation_off") // Luau::Parser doesn't exactly preserve the string representation of numbers in Lua, so we can find ourselves // falling out of sync with the original code. We need to push keywords out so that there's at least one space between them. const std::string code = R"( if math.abs(raySlope) < .01 then return 0 end )"; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { CHECK_EQ(code, transpile(code).code); } @@ -605,7 +607,7 @@ TEST_CASE("numbers") TEST_CASE("infinity") { const std::string code = R"( local a = 1e500 local b = 1e400 )"; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { CHECK_EQ(code, transpile(code).code); } @@ -618,21 +620,21 @@ TEST_CASE("infinity") TEST_CASE("numbers_with_separators") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local a = 123_456_789 )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("hexadecimal_numbers") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local a = 0xFFFF )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("binary_numbers") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local a = 0b0101 )"; CHECK_EQ(code, transpile(code).code); } @@ -645,28 +647,28 @@ TEST_CASE("single_quoted_strings") TEST_CASE("double_quoted_strings") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local a = "hello world" )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("simple_interp_string") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local a = `hello world` )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("raw_strings") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local a = [[ hello world ]] )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("raw_strings_with_blocks") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local a = [==[ hello world ]==] )"; CHECK_EQ(code, transpile(code).code); } @@ -685,7 +687,7 @@ TEST_CASE("escaped_strings_2") TEST_CASE("escaped_strings_newline") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( print("foo \ bar") @@ -695,14 +697,14 @@ TEST_CASE("escaped_strings_newline") TEST_CASE("escaped_strings_raw") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( local x = [=[\v<((do|load)file|require)\s*\(?['"]\zs[^'"]+\ze['"]]=] )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("position_correctly_updated_when_writing_multiline_string") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( call([[ testing @@ -748,56 +750,56 @@ TEST_CASE("function_call_parentheses_multiple_args_no_space") TEST_CASE("function_call_parentheses_multiple_args_space_before_commas") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( call(arg1 ,arg3 ,arg3) )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("function_call_spaces_before_parentheses") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( call () )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("function_call_spaces_within_parentheses") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( call( ) )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("function_call_string_double_quotes") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( call "string" )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("function_call_string_single_quotes") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( call 'string' )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("function_call_string_no_space") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( call'string' )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("function_call_table_literal") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( call { x = 1 } )"; CHECK_EQ(code, transpile(code).code); } TEST_CASE("function_call_table_literal_no_space") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( call{x=1} )"; CHECK_EQ(code, transpile(code).code); } @@ -842,7 +844,7 @@ TEST_CASE("emit_a_do_block_in_cases_of_potentially_ambiguous_syntax") TEST_CASE_FIXTURE(Fixture, "parentheses_multiline") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( local test = ( x @@ -855,7 +857,7 @@ local test = ( TEST_CASE_FIXTURE(Fixture, "stmt_semicolon") { ScopedFastFlag flags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, {FFlag::LuauExtendStatEndPosWithSemicolon, true}, }; std::string code = R"( local test = 1; )"; @@ -878,7 +880,7 @@ TEST_CASE_FIXTURE(Fixture, "do_block_ending_with_semicolon") TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon") { ScopedFastFlag flags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, {FFlag::LuauExtendStatEndPosWithSemicolon, true}, }; std::string code = R"( @@ -892,7 +894,7 @@ TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon") TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon_2") { ScopedFastFlag flags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, {FFlag::LuauExtendStatEndPosWithSemicolon, true}, }; std::string code = R"( @@ -904,7 +906,7 @@ TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon_2") TEST_CASE_FIXTURE(Fixture, "for_loop_stmt_semicolon") { ScopedFastFlag flags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, {FFlag::LuauExtendStatEndPosWithSemicolon, true}, }; std::string code = R"( @@ -917,7 +919,7 @@ TEST_CASE_FIXTURE(Fixture, "for_loop_stmt_semicolon") TEST_CASE_FIXTURE(Fixture, "while_do_semicolon") { ScopedFastFlag flags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, {FFlag::LuauExtendStatEndPosWithSemicolon, true}, }; std::string code = R"( @@ -930,7 +932,7 @@ TEST_CASE_FIXTURE(Fixture, "while_do_semicolon") TEST_CASE_FIXTURE(Fixture, "function_definition_semicolon") { ScopedFastFlag flags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, {FFlag::LuauExtendStatEndPosWithSemicolon, true}, }; std::string code = R"( @@ -1011,7 +1013,7 @@ TEST_CASE("always_emit_a_space_after_local_keyword") { std::string code = "do local aZZZZ = Workspace.P1.Shape local bZZZZ = Enum.PartType.Cylinder end"; - if (FFlag::LuauStoreCSTData) + if (FFlag::LuauStoreCSTData2) { CHECK_EQ(code, transpile(code).code); } @@ -1056,7 +1058,7 @@ TEST_CASE_FIXTURE(Fixture, "type_lists_should_be_emitted_correctly") end )"; - std::string expected = FFlag::LuauStoreCSTData ? R"( + std::string expected = FFlag::LuauStoreCSTData2 ? R"( local a:(a:string,b:number,...string)->(string,...number)=function(a:string,b:number,...:string): (string,...number) end @@ -1112,7 +1114,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_type_assertion") TEST_CASE_FIXTURE(Fixture, "type_assertion_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = "local a = 5 :: number"; CHECK_EQ(code, transpile(code, {}, true).code); @@ -1129,7 +1131,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else") TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = "local a = if 1 then 2 elseif 3 then 4 else 5"; CHECK_EQ(code, transpile(code).code); @@ -1137,7 +1139,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions") TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions_2") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( local x = if yes then nil @@ -1153,7 +1155,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions_2") TEST_CASE_FIXTURE(Fixture, "if_then_else_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = "local a = if 1 then 2 else 3"; CHECK_EQ(code, transpile(code).code); @@ -1190,7 +1192,7 @@ TEST_CASE_FIXTURE(Fixture, "if_then_else_spaces_around_tokens") TEST_CASE_FIXTURE(Fixture, "if_then_else_spaces_between_else_if") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( return if a then "was a" else @@ -1218,7 +1220,7 @@ local a: Import.Type TEST_CASE_FIXTURE(Fixture, "transpile_type_reference_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( local _: Foo.Type )"; CHECK_EQ(code, transpile(code, {}, true).code); @@ -1257,7 +1259,7 @@ local b: Packed<(number, string)> TEST_CASE_FIXTURE(Fixture, "type_packs_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type _ = Packed< T...> )"; CHECK_EQ(code, transpile(code, {}, true).code); @@ -1316,7 +1318,9 @@ TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested_3") { std::string code = "local a: nil | (string & number)"; - if (FFlag::LuauAstTypeGroup3) + if (FFlag::LuauStoreCSTData2) + CHECK_EQ(code, transpile(code, {}, true).code); + else if (FFlag::LuauAstTypeGroup3) CHECK_EQ("local a: (string & number)?", transpile(code, {}, true).code); else CHECK_EQ("local a: ( string & number)?", transpile(code, {}, true).code); @@ -1336,6 +1340,117 @@ TEST_CASE_FIXTURE(Fixture, "transpile_intersection_type_nested_2") CHECK_EQ(code, transpile(code, {}, true).code); } +TEST_CASE_FIXTURE(Fixture, "transpile_leading_union_pipe") +{ + ScopedFastFlag flags[] = { + {FFlag::LuauStoreCSTData2, true}, + {FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType, true}, + {FFlag::LuauParseOptionalAsNode2, true}, + }; + std::string code = "local a: | string | number"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: | string"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + +TEST_CASE_FIXTURE(Fixture, "transpile_union_spaces_around_tokens") +{ + ScopedFastFlag flags[] = { + {FFlag::LuauStoreCSTData2, true}, + {FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType, true}, + {FFlag::LuauParseOptionalAsNode2, true}, + }; + std::string code = "local a: string | number"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string | number"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + +TEST_CASE_FIXTURE(Fixture, "transpile_leading_intersection_ampersand") +{ + ScopedFastFlag flags[] = { + {FFlag::LuauStoreCSTData2, true}, + {FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType, true}, + }; + std::string code = "local a: & string & number"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: & string"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + +TEST_CASE_FIXTURE(Fixture, "transpile_intersection_spaces_around_tokens") +{ + ScopedFastFlag flags[] = { + {FFlag::LuauStoreCSTData2, true}, + {FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType, true}, + }; + std::string code = "local a: string & number"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string & number"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + +TEST_CASE_FIXTURE(Fixture, "transpile_mixed_union_intersection") +{ + ScopedFastFlag flags[] = { + {FFlag::LuauStoreCSTData2, true}, + {FFlag::LuauAstTypeGroup3, true}, + {FFlag::LuauParseOptionalAsNode2, true}, + }; + std::string code = "local a: string | (Foo & Bar)"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string | (Foo & Bar)"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string | ( Foo & Bar)"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string | (Foo & Bar )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string & (Foo | Bar)"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string & ( Foo | Bar)"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string & (Foo | Bar )"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + +TEST_CASE_FIXTURE(Fixture, "transpile_preserve_union_optional_style") +{ + ScopedFastFlag flags[] = { + {FFlag::LuauStoreCSTData2, true}, + {FFlag::LuauParseOptionalAsNode2, true}, + }; + std::string code = "local a: string | nil"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string?"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string???"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string? | nil"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string | nil | number"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string | nil | number?"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = "local a: string? | number?"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + TEST_CASE_FIXTURE(Fixture, "transpile_varargs") { std::string code = "local function f(...) return ... end"; @@ -1345,7 +1460,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_varargs") TEST_CASE_FIXTURE(Fixture, "index_name_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string one = "local _ = a.name"; CHECK_EQ(one, transpile(one, {}, true).code); @@ -1358,7 +1473,7 @@ TEST_CASE_FIXTURE(Fixture, "index_name_spaces_around_tokens") TEST_CASE_FIXTURE(Fixture, "index_name_ends_with_digit") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = "sparkles.Color = Color3.new()"; CHECK_EQ(code, transpile(code, {}, true).code); } @@ -1372,7 +1487,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_index_expr") TEST_CASE_FIXTURE(Fixture, "index_expr_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string one = "local _ = a[2]"; CHECK_EQ(one, transpile(one, {}, true).code); @@ -1416,7 +1531,7 @@ local _ = # e TEST_CASE_FIXTURE(Fixture, "binary_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( local _ = 1+1 local _ = 1 +1 @@ -1458,7 +1573,7 @@ a ..= ' - result' TEST_CASE_FIXTURE(Fixture, "compound_assignment_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string one = R"( a += 1 )"; CHECK_EQ(one, transpile(one, {}, true).code); @@ -1475,7 +1590,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_assign_multiple") TEST_CASE_FIXTURE(Fixture, "transpile_assign_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string one = "a = 1"; CHECK_EQ(one, transpile(one).code); @@ -1512,7 +1627,10 @@ TEST_CASE_FIXTURE(Fixture, "transpile_union_reverse") { std::string code = "local a: nil | number"; - CHECK_EQ("local a: number?", transpile(code, {}, true).code); + if (FFlag::LuauStoreCSTData2) + CHECK_EQ(code, transpile(code, {}, true).code); + else + CHECK_EQ("local a: number?", transpile(code, {}, true).code); } TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple") @@ -1628,7 +1746,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple_types") TEST_CASE_FIXTURE(Fixture, "transpile_string_interp") { ScopedFastFlag fflags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, }; std::string code = R"( local _ = `hello {name}` )"; @@ -1638,7 +1756,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp") TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline") { ScopedFastFlag fflags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, }; std::string code = R"( local _ = `hello { name @@ -1650,7 +1768,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline") TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_on_new_line") { ScopedFastFlag fflags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, }; std::string code = R"( error( @@ -1663,7 +1781,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_on_new_line") TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline_escape") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( local _ = `hello \ world!` )"; @@ -1673,7 +1791,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline_escape") TEST_CASE_FIXTURE(Fixture, "transpile_string_literal_escape") { ScopedFastFlag fflags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, }; std::string code = R"( local _ = ` bracket = \{, backtick = \` = {'ok'} ` )"; @@ -1689,7 +1807,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_type_functions") TEST_CASE_FIXTURE(Fixture, "transpile_type_functions_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type function foo() end )"; CHECK_EQ(code, transpile(code, {}, true).code); @@ -1705,7 +1823,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_type_functions_spaces_around_tokens") TEST_CASE_FIXTURE(Fixture, "transpile_typeof_spaces_around_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type X = typeof(x) )"; CHECK_EQ(code, transpile(code, {}, true).code); @@ -1730,14 +1848,14 @@ TEST_CASE("transpile_single_quoted_string_types") TEST_CASE("transpile_double_quoted_string_types") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( type a = "hello world" )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE("transpile_raw_string_types") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type a = [[ hello world ]] )"; CHECK_EQ(code, transpile(code, {}, true).code); @@ -1747,14 +1865,14 @@ TEST_CASE("transpile_raw_string_types") TEST_CASE("transpile_escaped_string_types") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( type a = "\\b\\t\\n\\\\" )"; CHECK_EQ(code, transpile(code, {}, true).code); } TEST_CASE("transpile_type_table_semicolon_separators") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; const std::string code = R"( type Foo = { bar: number; @@ -1766,7 +1884,7 @@ TEST_CASE("transpile_type_table_semicolon_separators") TEST_CASE("transpile_type_table_access_modifiers") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type Foo = { read bar: number, @@ -1787,7 +1905,7 @@ TEST_CASE("transpile_type_table_access_modifiers") TEST_CASE("transpile_type_table_spaces_between_tokens") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type Foo = { bar: number, } )"; CHECK_EQ(code, transpile(code, {}, true).code); @@ -1830,7 +1948,7 @@ TEST_CASE("transpile_type_table_spaces_between_tokens") TEST_CASE("transpile_type_table_preserve_original_indexer_style") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type Foo = { [number]: string @@ -1846,7 +1964,7 @@ TEST_CASE("transpile_type_table_preserve_original_indexer_style") TEST_CASE("transpile_type_table_preserve_indexer_location") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type Foo = { [number]: string, @@ -1875,7 +1993,7 @@ TEST_CASE("transpile_type_table_preserve_indexer_location") TEST_CASE("transpile_type_table_preserve_property_definition_style") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type Foo = { ["$$typeof1"]: string, @@ -1888,7 +2006,7 @@ TEST_CASE("transpile_type_table_preserve_property_definition_style") TEST_CASE("transpile_types_preserve_parentheses_style") { ScopedFastFlag flags[] = { - {FFlag::LuauStoreCSTData, true}, + {FFlag::LuauStoreCSTData2, true}, {FFlag::LuauAstTypeGroup3, true}, }; @@ -1929,7 +2047,7 @@ end TEST_CASE("transpile_type_function_unnamed_arguments") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type Foo = () -> () )"; CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); @@ -1963,7 +2081,7 @@ TEST_CASE("transpile_type_function_unnamed_arguments") TEST_CASE("transpile_type_function_named_arguments") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type Foo = (x: string) -> () )"; CHECK_EQ(R"( type Foo = (x: string) ->() )", transpile(code, {}, true).code); @@ -1991,7 +2109,7 @@ TEST_CASE("transpile_type_function_named_arguments") TEST_CASE("transpile_type_function_generics") { - ScopedFastFlag _{FFlag::LuauStoreCSTData, true}; + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; std::string code = R"( type Foo = () -> () )"; CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); @@ -2023,4 +2141,55 @@ TEST_CASE("transpile_type_function_generics") CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code); } +TEST_CASE("fuzzer_nil_optional") +{ + ScopedFastFlag _{FFlag::LuauParseOptionalAsNode2, true}; + + const std::string code = R"( local x: nil? )"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + +TEST_CASE("transpile_function_attributes") +{ + ScopedFastFlag _{FFlag::LuauStoreCSTData2, true}; + std::string code = R"( + @native + function foo() + end + )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( + @native + local function foo() + end + )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( + @checked local function foo() + end + )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( + local foo = @native function() end + )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( + @native + function foo:bar() + end + )"; + CHECK_EQ(code, transpile(code, {}, true).code); + + code = R"( + @native @checked + function foo:bar() + end + )"; + CHECK_EQ(code, transpile(code, {}, true).code); +} + TEST_SUITE_END(); diff --git a/tests/TypeFunction.test.cpp b/tests/TypeFunction.test.cpp index 2bda60f1..1356901e 100644 --- a/tests/TypeFunction.test.cpp +++ b/tests/TypeFunction.test.cpp @@ -17,7 +17,9 @@ LUAU_FASTFLAG(LuauIndexTypeFunctionImprovements) LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit) LUAU_FASTFLAG(LuauIndexTypeFunctionFunctionMetamethods) LUAU_FASTFLAG(LuauMetatableTypeFunctions) +LUAU_FASTFLAG(LuauMetatablesHaveLength) LUAU_FASTFLAG(LuauIndexAnyIsAny) +LUAU_FASTFLAG(LuauNewTypeFunReductionChecks2) struct TypeFunctionFixture : Fixture { @@ -145,19 +147,17 @@ TEST_CASE_FIXTURE(TypeFunctionFixture, "unsolvable_function") if (!FFlag::LuauSolverV2) return; + ScopedFastFlag luauNewTypeFunReductionChecks2{FFlag::LuauNewTypeFunReductionChecks2, true}; + CheckResult result = check(R"( local impossible: (Swap) -> Swap> local a = impossible(123) local b = impossible(true) )"); - LUAU_REQUIRE_ERROR_COUNT(6, result); - CHECK(toString(result.errors[0]) == "Type function instance Swap> is uninhabited"); - CHECK(toString(result.errors[1]) == "Type function instance Swap is uninhabited"); - CHECK(toString(result.errors[2]) == "Type function instance Swap> is uninhabited"); - CHECK(toString(result.errors[3]) == "Type function instance Swap is uninhabited"); - CHECK(toString(result.errors[4]) == "Type function instance Swap> is uninhabited"); - CHECK(toString(result.errors[5]) == "Type function instance Swap is uninhabited"); + LUAU_REQUIRE_ERROR_COUNT(2, result); + CHECK(toString(result.errors[0]) == "Type 'number' could not be converted into 'never'"); + CHECK(toString(result.errors[1]) == "Type 'boolean' could not be converted into 'never'"); } TEST_CASE_FIXTURE(TypeFunctionFixture, "table_internal_functions") @@ -1552,4 +1552,35 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_respects_metatable_metamethod") CHECK_EQ(toString(requireTypeAlias("Metatable")), "string"); } +TEST_CASE_FIXTURE(BuiltinsFixture, "type_function_correct_cycle_check") +{ + if (!FFlag::LuauSolverV2) + return; + + ScopedFastFlag luauNewTypeFunReductionChecks2{FFlag::LuauNewTypeFunReductionChecks2, true}; + + CheckResult result = check(R"( +type foo = { a: add, b : add } + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "len_typefun_on_metatable") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + ScopedFastFlag luauMetatablesHaveLength{FFlag::LuauMetatablesHaveLength, true}; + + CheckResult result = check(R"( +local t = setmetatable({}, { __mode = "v" }) + +local function f() + table.insert(t, {}) + print(#t * 100) +end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeFunction.user.test.cpp b/tests/TypeFunction.user.test.cpp index 9a4a7cd8..a313e039 100644 --- a/tests/TypeFunction.user.test.cpp +++ b/tests/TypeFunction.user.test.cpp @@ -10,9 +10,9 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAG(LuauTypeFunReadWriteParents) -LUAU_FASTFLAG(LuauTypeFunPrintFix) LUAU_FASTFLAG(LuauImproveTypePathsInErrors) LUAU_FASTFLAG(LuauUserTypeFunTypecheck) +LUAU_FASTFLAG(LuauNewTypeFunReductionChecks2) TEST_SUITE_BEGIN("UserDefinedTypeFunctionTests"); @@ -2030,7 +2030,7 @@ local _:test TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_print_tab_char_fix") { - ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauTypeFunPrintFix, true}}; + ScopedFastFlag solverV2{FFlag::LuauSolverV2, true}; CheckResult result = check(R"( type function test(t) @@ -2103,4 +2103,105 @@ end LUAU_REQUIRE_ERROR_COUNT(1, result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "outer_generics_irreducible") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + ScopedFastFlag luauNewTypeFunReductionChecks2{FFlag::LuauNewTypeFunReductionChecks2, true}; + + CheckResult result = check(R"( +type function func(t) + return t +end + +type wrap = { a: func } + +local x: wrap = nil :: any + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK(toString(requireType("x"), ToStringOptions{true}) == "{ a: string? }"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "inner_generics_reducible") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + + CheckResult result = check(R"( +type function func(t) + return t +end + +type wrap = { a: func<(T) -> number>, b: T } + +local x: wrap = nil :: any + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK(toString(requireType("x"), ToStringOptions{true}) == "{ a: (T) -> number, b: string }"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "blocking_nested_pending_expansions") +{ + if (!FFlag::LuauSolverV2) + return; + + CheckResult result = check(R"( +type function func(t) + return t +end + +type test = { x: T, y: T? } +type wrap = { a: func<(string, keyof>) -> number>, b: T } +local x: wrap +local y: keyof + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK(toString(requireType("x"), ToStringOptions{true}) == R"({ a: (string, "x" | "y") -> number, b: string })"); + CHECK(toString(requireType("y"), ToStringOptions{true}) == R"("a" | "b")"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "blocking_nested_pending_expansions_2") +{ + ScopedFastFlag newSolver{FFlag::LuauSolverV2, true}; + + CheckResult result = check(R"( +type function foo(t) + return types.unionof(t, types.singleton(nil)) +end + +local x: foo<{a: foo, b: foo}> = nil + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK(toString(requireType("x"), ToStringOptions{true}) == "{ a: string?, b: number? }?"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "irreducible_pending_expansions") +{ + if (!FFlag::LuauSolverV2) + return; + + ScopedFastFlag luauNewTypeFunReductionChecks2{FFlag::LuauNewTypeFunReductionChecks2, true}; + + CheckResult result = check(R"( +type function foo(t) + return types.unionof(t, types.singleton(nil)) +end + +type table = { a: index } +type wrap = foo> + +local x: wrap<{a: number}> = { a = 2 } + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK(toString(requireType("x"), ToStringOptions{true}) == "{ a: number }?"); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index ca2da4e3..81ddda60 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -12,6 +12,10 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauFixInfiniteRecursionInNormalization) LUAU_FASTFLAG(LuauImproveTypePathsInErrors) +LUAU_FASTFLAG(LuauPrecalculateMutatedFreeTypes2) +LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment) +LUAU_FASTFLAG(LuauBidirectionalInferenceUpcast) +LUAU_FASTFLAG(LuauBidirectionalInferenceCollectIndexerTypes) TEST_SUITE_BEGIN("TypeAliases"); @@ -253,8 +257,12 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") TEST_CASE_FIXTURE(Fixture, "mutually_recursive_generic_aliases") { - // CLI-116108 - DOES_NOT_PASS_NEW_SOLVER_GUARD(); + ScopedFastFlag sffs[] = { + {FFlag::LuauPrecalculateMutatedFreeTypes2, true}, + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + {FFlag::LuauBidirectionalInferenceUpcast, true}, + {FFlag::LuauBidirectionalInferenceCollectIndexerTypes, true}, + }; CheckResult result = check(R"( --!strict diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index 58815ac0..5c2b752f 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -24,6 +24,7 @@ LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTFLAG(DebugLuauEqSatSimplification) LUAU_FASTFLAG(LuauUngeneralizedTypesForRecursiveFunctions) LUAU_FASTFLAG(LuauImproveTypePathsInErrors) +LUAU_FASTFLAG(LuauReduceUnionFollowUnionType) TEST_SUITE_BEGIN("TypeInferFunctions"); @@ -3191,4 +3192,30 @@ TEST_CASE_FIXTURE(Fixture, "recursive_function_calls_should_not_use_the_generali LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "fuzz_unwind_mutually_recursive_union_type_func") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauReduceUnionFollowUnionType, true} + }; + + // This block ends up minting a type like: + // + // t2 where t1 = union | union | union ; t2 = union + // + CheckResult result = check(R"( + local _ = ... + function _() + _ = _ + end + _[function(...) repeat until _(_[l100]) _ = _ end] += _ + )"); + LUAU_REQUIRE_ERROR_COUNT(2, result); + auto err0 = get(result.errors[0]); + CHECK(err0); + CHECK_EQ(err0->name, "l100"); + auto err1 = get(result.errors[1]); + CHECK(err1); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index 537091f0..38d349ad 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -13,7 +13,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(DebugLuauEqSatSimplification) -LUAU_FASTFLAG(LuauStoreCSTData) +LUAU_FASTFLAG(LuauStoreCSTData2) LUAU_FASTINT(LuauNormalizeCacheLimit) LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTINT(LuauTypeInferIterationLimit) @@ -49,7 +49,7 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete") end )"; - const std::string expected = FFlag::LuauStoreCSTData ? R"( + const std::string expected = FFlag::LuauStoreCSTData2 ? R"( function f(a:{fn:()->(a,b...)}): () if type(a) == 'boolean' then local a1:boolean=a @@ -68,7 +68,7 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete") end )"; - const std::string expectedWithNewSolver = FFlag::LuauStoreCSTData ? R"( + const std::string expectedWithNewSolver = FFlag::LuauStoreCSTData2 ? R"( function f(a:{fn:()->(unknown,...unknown)}): () if type(a) == 'boolean' then local a1:{fn:()->(unknown,...unknown)}&boolean=a @@ -87,7 +87,7 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete") end )"; - const std::string expectedWithEqSat = FFlag::LuauStoreCSTData ? R"( + const std::string expectedWithEqSat = FFlag::LuauStoreCSTData2 ? R"( function f(a:{fn:()->(unknown,...unknown)}): () if type(a) == 'boolean' then local a1:{fn:()->(unknown,...unknown)}&boolean=a diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 270707a5..59072d0f 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -14,6 +14,7 @@ LUAU_FASTFLAG(LuauIntersectNotNil) LUAU_FASTFLAG(LuauSkipNoRefineDuringRefinement) LUAU_FASTFLAG(LuauFunctionCallsAreNotNilable) LUAU_FASTFLAG(LuauDoNotLeakNilInRefinement) +LUAU_FASTFLAG(LuauSimplyRefineNotNil) using namespace Luau; @@ -756,14 +757,24 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "nonoptional_type_can_narrow_to_nil_if_sense_ { // CLI-115281 Types produced by refinements do not consistently get simplified CHECK_EQ("(nil & string)?", toString(requireTypeAtPosition({4, 24}))); // type(v) == "nil" - CHECK_EQ( - "(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({6, 24})) - ); // type(v) ~= "nil" + + if (FFlag::LuauSimplyRefineNotNil) + CHECK_EQ( + "string & ~nil", toString(requireTypeAtPosition({6, 24})) + ); // type(v) ~= "nil" + else + CHECK_EQ( + "(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({6, 24})) + ); // type(v) ~= "nil" CHECK_EQ("(nil & string)?", toString(requireTypeAtPosition({10, 24}))); // equivalent to type(v) == "nil" - CHECK_EQ( - "(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({12, 24})) - ); // equivalent to type(v) ~= "nil" + + if (FFlag::LuauSimplyRefineNotNil) + CHECK_EQ("string & ~nil", toString(requireTypeAtPosition({12, 24}))); // equivalent to type(v) ~= "nil" + else + CHECK_EQ( + "(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({12, 24})) + ); // equivalent to type(v) ~= "nil" } else { diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 91192eb1..9d3e0338 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -7,6 +7,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauPropagateExpectedTypesForCalls) LUAU_FASTFLAG(LuauImproveTypePathsInErrors) TEST_SUITE_BEGIN("TypeSingletons"); @@ -152,6 +153,26 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_function_call_with_singletons") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "overloaded_function_resolution_singleton_parameters") +{ + ScopedFastFlag sff{FFlag::LuauPropagateExpectedTypesForCalls, true}; + + CheckResult result = check(R"( + type A = ("A") -> string + type B = ("B") -> number + + local function foo(f: A & B) + return f("A"), f("B") + end + )"); + LUAU_REQUIRE_NO_ERRORS(result); + TypeId t = requireType("foo"); + const FunctionType* fooType = get(requireType("foo")); + REQUIRE(fooType != nullptr); + + CHECK(toString(t) == "(((\"A\") -> string) & ((\"B\") -> number)) -> (string, number)"); +} + TEST_CASE_FIXTURE(Fixture, "overloaded_function_call_with_singletons_mismatch") { DOES_NOT_PASS_NEW_SOLVER_GUARD(); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index f4744067..636441e2 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -30,6 +30,8 @@ LUAU_FASTFLAG(LuauBidirectionalInferenceUpcast) LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint) LUAU_FASTFLAG(LuauSearchForRefineableType) LUAU_FASTFLAG(LuauImproveTypePathsInErrors) +LUAU_FASTFLAG(LuauBidirectionalInferenceCollectIndexerTypes) +LUAU_FASTFLAG(LuauBidirectionalFailsafe) TEST_SUITE_BEGIN("TableTests"); @@ -5168,34 +5170,35 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_union_type") TEST_CASE_FIXTURE(Fixture, "function_check_constraint_too_eager") { - // NOTE: All of these examples should have no errors, but - // bidirectional inference is known to be broken. ScopedFastFlag sffs[] = { {FFlag::LuauSolverV2, true}, {FFlag::LuauPrecalculateMutatedFreeTypes2, true}, + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + {FFlag::LuauBidirectionalInferenceCollectIndexerTypes, true}, }; - auto result = check(R"( + CheckResult result = check(R"( local function doTheThing(_: { [string]: unknown }) end doTheThing({ ['foo'] = 5, ['bar'] = 'heyo', }) )"); - LUAU_CHECK_ERROR_COUNT(1, result); - LUAU_CHECK_NO_ERROR(result, ConstraintSolvingIncompleteError); - LUAU_CHECK_ERROR_COUNT(1, check(R"( + LUAU_REQUIRE_NO_ERRORS(result); + + result = check(R"( type Input = { [string]: unknown } local i : Input = { [('%s'):format('3.14')]=5, ['stringField']='Heyo' } - )")); + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + - // This example previously asserted due to eagerly mutating the underlying - // table type. result = check(R"( type Input = { [string]: unknown } @@ -5206,8 +5209,45 @@ TEST_CASE_FIXTURE(Fixture, "function_check_constraint_too_eager") ['stringField']='Heyo' }) )"); - LUAU_CHECK_ERROR_COUNT(1, result); - LUAU_CHECK_NO_ERROR(result, ConstraintSolvingIncompleteError); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "magic_functions_bidirectionally_inferred") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPrecalculateMutatedFreeTypes2, true}, + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + {FFlag::LuauBidirectionalInferenceCollectIndexerTypes, true}, + }; + + CheckResult result = check(R"( + local function getStuff(): (string, number, string) + return "hello", 42, "world" + end + local t: { [string]: number } = { + [select(1, getStuff())] = select(2, getStuff()), + [select(3, getStuff())] = select(2, getStuff()) + } + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + result = check(R"( + local function getStuff(): (string, number, string) + return "hello", 42, "world" + end + local t: { [string]: number } = { + [select(1, getStuff())] = select(2, getStuff()), + [select(3, getStuff())] = select(3, getStuff()) + } + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + auto err = get(result.errors[0]); + CHECK_EQ("{ [string]: number | string }", toString(err->givenType)); + CHECK_EQ("{ [string]: number }", toString(err->wantedType)); } @@ -5471,6 +5511,8 @@ TEST_CASE_FIXTURE(Fixture, "missing_fields_bidirectional_inference") {FFlag::LuauSolverV2, true}, {FFlag::LuauPrecalculateMutatedFreeTypes2, true}, {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + {FFlag::LuauBidirectionalInferenceUpcast, true}, + {FFlag::LuauBidirectionalInferenceCollectIndexerTypes, true}, }; auto result = check(R"( @@ -5478,7 +5520,8 @@ TEST_CASE_FIXTURE(Fixture, "missing_fields_bidirectional_inference") local b: Book = { title = "The Odyssey" } local t: { Book } = { { title = "The Illiad", author = "Homer" }, - { author = "Virgil" } + { title = "Inferno", author = "Virgil" }, + { author = "Virgil" }, } )"); @@ -5490,12 +5533,49 @@ TEST_CASE_FIXTURE(Fixture, "missing_fields_bidirectional_inference") CHECK_EQ(result.errors[0].location, Location{{2, 24}, {2, 49}}); err = get(result.errors[1]); REQUIRE(err); - CHECK_EQ(toString(err->givenType), "{{ author: string } | { author: string, title: string }}"); + // CLI-144203: This could be better. + CHECK_EQ(toString(err->givenType), "{{ author: string }}"); CHECK_EQ(toString(err->wantedType), "{Book}"); - CHECK_EQ(result.errors[1].location, Location{{3, 28}, {6, 9}}); + CHECK_EQ(result.errors[1].location, Location{{3, 28}, {7, 9}}); } +TEST_CASE_FIXTURE(Fixture, "generic_index_syntax_bidirectional_infer_with_tables") +{ + ScopedFastFlag sffs[] = { + {FFlag::LuauSolverV2, true}, + {FFlag::LuauPrecalculateMutatedFreeTypes2, true}, + {FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true}, + {FFlag::LuauBidirectionalInferenceUpcast, true}, + {FFlag::LuauBidirectionalInferenceCollectIndexerTypes, true}, + }; + + auto result = check((R"( + local function getStatus(): string + return "Yeah can you look in returned books?" + end + local function getPratchettStatus() + return { isLate = true } + end + type Status = { isLate: boolean, daysLate: number? } + local key1 = "Great Expecations" + local key2 = "The Outsiders" + local key3 = "Guards! Guards!" + local books: { [string]: Status } = { + [key1] = { isLate = true, daysLate = "coconut" }, + [key2] = getStatus(), + [key3] = getPratchettStatus() + } + )")); + + LUAU_CHECK_ERROR_COUNT(1, result); + auto err = get(result.errors[0]); + REQUIRE(err); + // NOTE: This is because the inferred keys of `books` are all primitive types. + CHECK_EQ(toString(err->givenType), "{ [string | string | string]: string | { daysLate: string, isLate: boolean } | { isLate: boolean } }"); + CHECK_EQ(toString(err->wantedType), "{ [string]: Status }"); +} + TEST_CASE_FIXTURE(Fixture, "deeply_nested_classish_inference") { ScopedFastFlag sffs[] = { @@ -5572,5 +5652,43 @@ TEST_CASE_FIXTURE(Fixture, "bigger_nested_table_causes_big_type_error") CHECK_EQ(expected, toString(result.errors[0])); } +TEST_CASE_FIXTURE(Fixture, "unsafe_bidirectional_mutation") +{ + ScopedFastFlag _{FFlag::LuauBidirectionalFailsafe, true}; + // It's kind of suspect that we allow multiple definitions of keys in + // a single table. + LUAU_REQUIRE_NO_ERRORS(check(R"( + type F = { + _G: () -> () + } + function _() + return + end + local function h(f: F) end + h({ + _G = {}, + _G = _, + }) + )")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "function_call_in_indexer_with_compound_assign") +{ + ScopedFastFlag _{FFlag::LuauBidirectionalFailsafe, true}; + // This has a bunch of errors, we really just need it to not crash / assert. + std::ignore = check(R"( + --!strict + local _ = 7143424 + _[ + setfenv( + ..., + { + n0 = _, + } + ) + ] *= _ + )"); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index a8d82ab9..010d9bcf 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -30,6 +30,7 @@ LUAU_FASTFLAG(LuauInferLocalTypesInMultipleAssignments) LUAU_FASTFLAG(LuauUnifyMetatableWithAny) LUAU_FASTFLAG(LuauExtraFollows) LUAU_FASTFLAG(LuauImproveTypePathsInErrors) +LUAU_FASTFLAG(LuauTypeCheckerAcceptNumberConcats) using namespace Luau; @@ -1862,7 +1863,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_works_with_any") end, } )")); - } TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_infer_any_ret") @@ -1937,14 +1937,14 @@ end TEST_CASE_FIXTURE(Fixture, "concat_string_with_string_union") { ScopedFastFlag _{FFlag::LuauSolverV2, true}; + ScopedFastFlag fixNumberConcats{FFlag::LuauTypeCheckerAcceptNumberConcats, true}; + LUAU_REQUIRE_NO_ERRORS(check(R"( - local function foo(n : number): string return "" end - local function bar(n: number, m: string) end - local function concat_stuff(x, y) - local z = foo(x) - bar(y, z) + local function concat_stuff(x: string, y : string | number) + return x .. y end )")); } + TEST_SUITE_END();