diff --git a/Loom.Core/Diagnostics/InternalCodes.cs b/Loom.Core/Diagnostics/InternalCodes.cs index d7d61b3e..1cf30146 100644 --- a/Loom.Core/Diagnostics/InternalCodes.cs +++ b/Loom.Core/Diagnostics/InternalCodes.cs @@ -74,6 +74,7 @@ public static class InternalCodes public const string InvalidLuauNameAttribute = "L350"; public const string AnonymousEventDisconnect = "L351"; public const string UnresolvedEventDisconnect = "L352"; - + public const string ReservedLuauKeyword = "L353"; + public const string SimplifiableCode = "L400"; } diff --git a/Loom.Core/FlowAnalysis/FlowAnalyzer.cs b/Loom.Core/FlowAnalysis/FlowAnalyzer.cs index bac345bd..244cf346 100644 --- a/Loom.Core/FlowAnalysis/FlowAnalyzer.cs +++ b/Loom.Core/FlowAnalysis/FlowAnalyzer.cs @@ -12,7 +12,11 @@ public sealed class FlowAnalyzer(SemanticModel semanticModel) private readonly Dictionary _states = []; public FlowState GetState(Node node) => _states.TryGetValue(node, out var existingState) ? existingState : FlowState.Empty; - public FlowAnalyzerResult Analyze() => new(BindState(semanticModel.Tree, AnalyzeStatements(semanticModel.Tree.Statements, new FlowState())), _diagnostics); + public FlowAnalyzerResult Analyze() + { + BindState(semanticModel.Tree, AnalyzeStatements(semanticModel.Tree.Statements, new FlowState())); + return new FlowAnalyzerResult(_diagnostics); + } private FlowState AnalyzeStatements(IReadOnlyList statements, FlowState state) => statements.Aggregate(state, (current, statement) => AnalyzeStatement(statement, current)); @@ -26,6 +30,7 @@ private FlowState AnalyzeStatement(Statement statement, FlowState state) FunctionDeclaration functionDeclaration => AnalyzeFunctionDeclaration(functionDeclaration, state), EventDeclaration eventDeclaration => AnalyzeEventDeclaration(eventDeclaration, state), InterfaceDeclaration interfaceDeclaration => AnalyzeInterfaceDeclaration(interfaceDeclaration, state), + EnumDeclaration enumDeclaration => AnalyzeEnumDeclaration(enumDeclaration, state), Implement implement => AnalyzeImplement(implement, state), Return @return => AnalyzeReturn(@return, state), Break @break => AnalyzeBreak(@break, state), @@ -101,6 +106,14 @@ private FlowState AnalyzeInterfaceDeclaration(InterfaceDeclaration interfaceDecl : state ); + private FlowState AnalyzeEnumDeclaration(EnumDeclaration enumDeclaration, FlowState state) => + BindState( + enumDeclaration, + semanticModel.GetDeclarationSymbol(enumDeclaration, SymbolKind.Variable) is { } symbol + ? state.WithInitialized(symbol) + : state + ); + private FlowState AnalyzeFunctionDeclaration(FunctionDeclaration functionDeclaration, FlowState state) { var newState = semanticModel.GetDeclarationSymbol(functionDeclaration) is { } symbol diff --git a/Loom.Core/FlowAnalysis/FlowAnalyzerResult.cs b/Loom.Core/FlowAnalysis/FlowAnalyzerResult.cs index 818bea91..3c8daab8 100644 --- a/Loom.Core/FlowAnalysis/FlowAnalyzerResult.cs +++ b/Loom.Core/FlowAnalysis/FlowAnalyzerResult.cs @@ -2,5 +2,5 @@ namespace Loom.Core.FlowAnalysis; -public sealed record FlowAnalyzerResult(FlowState FlowState, DiagnosticBag Diagnostics) +public sealed record FlowAnalyzerResult(DiagnosticBag Diagnostics) : DiagnosedResult(Diagnostics); \ No newline at end of file diff --git a/Loom.Core/Generation/LuauGenerator.Types.cs b/Loom.Core/Generation/LuauGenerator.Types.cs index af502e91..c6ca599e 100644 --- a/Loom.Core/Generation/LuauGenerator.Types.cs +++ b/Loom.Core/Generation/LuauGenerator.Types.cs @@ -1,7 +1,6 @@ using Loom.Core.Diagnostics; using Loom.Core.Parsing.AST; using Loom.Core.Resolving; -using Loom.Core.TypeChecking.Types; using Loom.Luau; using Loom.Luau.AST; using ArrayType = Loom.Core.Parsing.AST.ArrayType; diff --git a/Loom.Core/Generation/Macros/MacroExpander.cs b/Loom.Core/Generation/Macros/MacroExpander.cs index 87389b57..c6c709fd 100644 --- a/Loom.Core/Generation/Macros/MacroExpander.cs +++ b/Loom.Core/Generation/Macros/MacroExpander.cs @@ -4,7 +4,6 @@ using Loom.Core.Parsing.AST; using Loom.Core.Resolving; using Loom.Core.TypeChecking; -using Loom.Core.TypeChecking.Types; using Loom.Luau.AST; using ElementAccess = Loom.Core.Parsing.AST.ElementAccess; using Identifier = Loom.Core.Parsing.AST.Identifier; diff --git a/Loom.Core/Lexing/Lexer.cs b/Loom.Core/Lexing/Lexer.cs index e5db86f5..3bd1479b 100644 --- a/Loom.Core/Lexing/Lexer.cs +++ b/Loom.Core/Lexing/Lexer.cs @@ -24,7 +24,7 @@ public LexerResult Tokenize() significantTokens.Add(token); } - return new LexerResult(file, significantTokens, allTokens, _diagnostics); + return new LexerResult(significantTokens, allTokens, _diagnostics); } private IEnumerable GetTokens() diff --git a/Loom.Core/Lexing/LexerResult.cs b/Loom.Core/Lexing/LexerResult.cs index be69a667..bca2afca 100644 --- a/Loom.Core/Lexing/LexerResult.cs +++ b/Loom.Core/Lexing/LexerResult.cs @@ -3,5 +3,5 @@ namespace Loom.Core.Lexing; -public sealed record LexerResult(SourceFile File, List Tokens, List TokensWithTrivia, DiagnosticBag Diagnostics) +public sealed record LexerResult(List Tokens, List TokensWithTrivia, DiagnosticBag Diagnostics) : DiagnosedResult(Diagnostics); \ No newline at end of file diff --git a/Loom.Core/Parsing/AST/EventDeclaration.cs b/Loom.Core/Parsing/AST/EventDeclaration.cs index 6043ec3b..a5032d2e 100644 --- a/Loom.Core/Parsing/AST/EventDeclaration.cs +++ b/Loom.Core/Parsing/AST/EventDeclaration.cs @@ -4,7 +4,7 @@ namespace Loom.Core.Parsing.AST; -public class EventDeclaration(Token keyword, Token name, TypeParameters? typeParameters, Parameters? parameters, Attributes? attributes = null) +public class EventDeclaration(Token keyword, Token name, TypeParameters? typeParameters, Parameters? parameters, Attributes? attributes) : GenericNamedDeclaration([], keyword, name, typeParameters, attributes), IWithAttributes { diff --git a/Loom.Core/Parsing/AST/Node.cs b/Loom.Core/Parsing/AST/Node.cs index 56c9be21..7b3e632a 100644 --- a/Loom.Core/Parsing/AST/Node.cs +++ b/Loom.Core/Parsing/AST/Node.cs @@ -29,7 +29,21 @@ protected Node(IEnumerable theseTokens, IEnumerable children) public abstract T Accept(Visitor visitor); public override string ToString() => LocationSpan.GetText().ToString(); public IReadOnlyList GetDescendants() where T : Node => GetDescendants().OfType().ToArray(); - public IReadOnlyList GetDescendants() => [..Children, ..Children.SelectMany(c => c.GetDescendants())]; + + public IReadOnlyList GetDescendants() + { + var result = new List(); + var queue = new Queue(Children); + while (queue.Count > 0) + { + var node = queue.Dequeue(); + result.Add(node); + foreach (var child in node.Children) + queue.Enqueue(child); + } + + return result; + } public bool IsDescendantOf() where T : Node => FirstAncestorOfType() != null; public T? FirstAncestorOfType() diff --git a/Loom.Core/Resolving/Resolver.cs b/Loom.Core/Resolving/Resolver.cs index 8b30e121..40dcb2e8 100644 --- a/Loom.Core/Resolving/Resolver.cs +++ b/Loom.Core/Resolving/Resolver.cs @@ -4,6 +4,7 @@ using Loom.Core.Parsing.AST; using Loom.Core.Text; using Loom.Core.TypeChecking; +using Loom.Luau; using Attribute = Loom.Core.Parsing.AST.Attribute; namespace Loom.Core.Resolving; @@ -689,6 +690,15 @@ private void DeclareSymbol(Symbol symbol) { AddToLookup(symbol); AddDeclaration(symbol); + if (LuauFactory.Keywords.Contains(symbol.Name)) + { + _diagnostics.Error( + symbol.Declaration, + InternalCodes.ReservedLuauKeyword, + $"'{symbol.Name}' is a reserved Luau keyword and cannot be used as a declaration name." + ); + } + if (TypeChecker.EmitDebugDiagnostics) _diagnostics.Debug(symbol.Declaration, $"Declared symbol: {symbol}"); diff --git a/Loom.Core/Resolving/SemanticModel.cs b/Loom.Core/Resolving/SemanticModel.cs index ae3fd037..0bd965f8 100644 --- a/Loom.Core/Resolving/SemanticModel.cs +++ b/Loom.Core/Resolving/SemanticModel.cs @@ -11,16 +11,6 @@ namespace Loom.Core.Resolving; public sealed record SemanticModel(Tree Tree, DiagnosticBag Diagnostics, SymbolTable Declarations, SymbolTable References) : DiagnosedResult(Diagnostics) { - internal int RuntimeReferences = 0; - - /// - /// Node IDs of reference-site nodes that originate from a non-intrinsic source file. - /// Populated by the resolver as references are recorded, so - /// can filter references by the file of the - /// referencing node without holding on to the node instances themselves. - /// - internal HashSet NonIntrinsicReferenceNodes { get; } = []; - public bool DisableRuntimeLibraryImport { get; set; } public bool MustImportRuntimeLibrary => !DisableRuntimeLibraryImport @@ -32,7 +22,16 @@ public sealed record SemanticModel(Tree Tree, DiagnosticBag Diagnostics, SymbolT ) ); + /// + /// Node IDs of reference-site nodes that originate from a non-intrinsic source file. + /// Populated by the resolver as references are recorded, so + /// can filter references by the file of the + /// referencing node without holding on to the node instances themselves. + /// + internal HashSet NonIntrinsicReferenceNodes { get; } = []; + internal int RuntimeReferences = 0; internal TypeSolver TypeSolver { get; } = new(new DiagnosticBag()); + private SymbolLookup DeclarationsByName => field ??= Declarations.Values.SelectMany(s => s).GroupBy(s => s.Name).ToDictionary(g => g.Key, g => g.ToList()); public bool IsCompileTimeConstant(Expression expression) => expression is Literal or NameOf @@ -43,7 +42,7 @@ expression is Literal or NameOf public object? GetConstantValue(Expression expression) => expression switch { - QualifiedName qn when GetType(qn.Identifier) is TypeChecking.Types.ObjectType objectType + QualifiedName qn when GetType(qn.Identifier) is ObjectType objectType && objectType.GetProperty(qn.Names.First().Name.Text) is { ValueType: TypeChecking.Types.LiteralType literalType } => literalType.Value, _ when GetType(expression) is TypeChecking.Types.LiteralType literalType => literalType.Value, @@ -114,12 +113,6 @@ public bool TryGetIntrinsicAttribute(Expression expression, string name, [MaybeN public Type GetType(Node node) => TypeSolver.GetType(node); public Type? GetDeclarationType(Node node) => GetSymbol(node) is { } symbol ? TypeSolver.GetType(symbol.Declaration) : null; public T? FindIntrinsicDeclarationSymbol(string name) where T : Symbol => FindDeclarationSymbol(name, s => s.IsIntrinsic); - - private Dictionary>? _declarationsByName; - - private Dictionary> DeclarationsByName => - _declarationsByName ??= Declarations.Values.SelectMany(s => s).GroupBy(s => s.Name).ToDictionary(g => g.Key, g => g.ToList()); - private T? FindDeclarationSymbol(string name) where T : Symbol => FindDeclarationSymbol(name, static _ => true); private T? FindDeclarationSymbol(string name, Func predicate) where T : Symbol => diff --git a/Loom.Core/TypeChecking/BinaryOperatorBinder.cs b/Loom.Core/TypeChecking/BinaryOperatorBinder.cs index 941aa7af..579b96d1 100644 --- a/Loom.Core/TypeChecking/BinaryOperatorBinder.cs +++ b/Loom.Core/TypeChecking/BinaryOperatorBinder.cs @@ -1,5 +1,4 @@ using Loom.Core.Parsing.AST; -using Loom.Core.Resolving; using Loom.Core.Text; using PrimitiveType = Loom.Core.TypeChecking.Types.PrimitiveType; using Type = Loom.Core.TypeChecking.Types.Type; diff --git a/Loom.Core/TypeChecking/Intrinsic/generated/None.loom b/Loom.Core/TypeChecking/Intrinsic/generated/None.loom index 617ebd88..b713a791 100644 --- a/Loom.Core/TypeChecking/Intrinsic/generated/None.loom +++ b/Loom.Core/TypeChecking/Intrinsic/generated/None.loom @@ -9448,15 +9448,15 @@ declare interface DragDetector: ClickDetector { [luau_name("WorldSecondaryAxis")] mut world_secondary_axis: Vector3; [luau_name("AddConstraintFunction"), luau_method] - add_constraint_function: fn(priority: number, function: fn: void): ScriptConnection; + add_constraint_function: fn(priority: number, callback: fn: void): ScriptConnection; [luau_name("GetReferenceFrame"), luau_method] get_reference_frame: fn: CFrame; [luau_name("RestartDrag"), luau_method] restart_drag: fn: void; [luau_name("SetDragStyleFunction"), luau_method] - set_drag_style_function: fn(function: fn: void): void; + set_drag_style_function: fn(callback: fn: void): void; [luau_name("SetPermissionPolicyFunction"), luau_method] - set_permission_policy_function: fn(function: fn: void): void; + set_permission_policy_function: fn(callback: fn: void): void; [luau_name("DragContinue")] event drag_continue(playerWhoDragged: Player, cursorRay: Ray, viewFrame: CFrame, vrInputFrame: CFrame?, isModeSwitchKeyDown: bool); [luau_name("DragEnd")] @@ -14077,9 +14077,9 @@ declare interface Model: PVInstance { #: A logical grouping of datamodel nodes which can enable scripts to run in parallel. :# declare interface Actor: Model { [luau_name("BindToMessage"), luau_method] - bind_to_message: fn(topic: string, function: fn: void): ScriptConnection; + bind_to_message: fn(topic: string, callback: fn: void): ScriptConnection; [luau_name("BindToMessageParallel"), luau_method] - bind_to_message_parallel: fn(topic: string, function: fn: void): ScriptConnection; + bind_to_message_parallel: fn(topic: string, callback: fn: void): ScriptConnection; [luau_name("SendMessage"), luau_method] send_message: fn(topic: string, message: unknown): void; } @@ -15199,9 +15199,9 @@ declare interface RunService: Instance { [luau_name("FrameNumber")] frame_number: number; [luau_name("BindToRenderStep"), luau_method] - bind_to_render_step: fn(name: string, priority: number, function: fn: void): void; + bind_to_render_step: fn(name: string, priority: number, callback: fn: void): void; [luau_name("BindToSimulation"), luau_method] - bind_to_simulation: fn(function: fn: void, frequency: Enum["StepFrequency"]?, priority: number?): ScriptConnection; + bind_to_simulation: fn(callback: fn: void, frequency: Enum["StepFrequency"]?, priority: number?): ScriptConnection; [luau_name("GetPredictionStatus"), luau_method] get_prediction_status: fn(context: Instance): Enum["PredictionStatus"]; [luau_name("IsClient"), luau_method] @@ -15466,7 +15466,7 @@ declare interface DataModel: ServiceProvider { [luau_name("RunService")] run_service: RunService?; [luau_name("BindToClose"), luau_method] - bind_to_close: fn(function: fn: void): void; + bind_to_close: fn(callback: fn: void): void; [luau_name("GetMessage"), luau_method] get_message: fn: string; [luau_name("GetRemoteBuildMode"), luau_method] @@ -16866,13 +16866,13 @@ declare interface UIDragDetector: UIComponent { [luau_name("UIDragSpeedAxisMapping")] mut ui_drag_speed_axis_mapping: Enum["UIDragSpeedAxisMapping"]; [luau_name("AddConstraintFunction"), luau_method] - add_constraint_function: fn(priority: number, function: fn: void): ScriptConnection; + add_constraint_function: fn(priority: number, callback: fn: void): ScriptConnection; [luau_name("GetReferencePosition"), luau_method] get_reference_position: fn: UDim2; [luau_name("GetReferenceRotation"), luau_method] get_reference_rotation: fn: number; [luau_name("SetDragStyleFunction"), luau_method] - set_drag_style_function: fn(function: fn: void): void; + set_drag_style_function: fn(callback: fn: void): void; [luau_name("DragContinue")] event drag_continue(inputPosition: Vector2); [luau_name("DragEnd")] @@ -16928,7 +16928,7 @@ declare interface UIGridStyleLayout: UILayout { [luau_name("ApplyLayout"), luau_method] apply_layout: fn: void; [luau_name("SetCustomSortFunction"), luau_method] - set_custom_sort_function: fn(function: fn: void?): void; + set_custom_sort_function: fn(callback: fn: void?): void; } declare interface UIGridLayout: UIGridStyleLayout { diff --git a/Loom.Core/TypeChecking/Intrinsic/generated/PluginSecurity.loom b/Loom.Core/TypeChecking/Intrinsic/generated/PluginSecurity.loom index 3d0aa475..8bb1915d 100644 --- a/Loom.Core/TypeChecking/Intrinsic/generated/PluginSecurity.loom +++ b/Loom.Core/TypeChecking/Intrinsic/generated/PluginSecurity.loom @@ -8294,7 +8294,7 @@ declare interface PluginGui: LayerCollector { [luau_name("Title")] mut title: string; [luau_name("BindToClose"), luau_method] - bind_to_close: fn(function: fn: void?): void; + bind_to_close: fn(callback: fn: void?): void; [luau_name("GetRelativeMousePosition"), luau_method] get_relative_mouse_position: fn: Vector2; [luau_name("PluginDragDropped")] diff --git a/Loom.Core/TypeChecking/TypeChecker.cs b/Loom.Core/TypeChecking/TypeChecker.cs index e6c3f457..27215989 100644 --- a/Loom.Core/TypeChecking/TypeChecker.cs +++ b/Loom.Core/TypeChecking/TypeChecker.cs @@ -1089,17 +1089,32 @@ private Type GetReturnType(FunctionDeclaration functionDeclaration) var possibleReturnTypes = functionDeclaration.Body is ExpressionBody body ? [Visit(body)] - : functionDeclaration.Body - .GetDescendants() - .Where(returnStatement => returnStatement.FirstAncestorOfType() == functionDeclaration) - .Select(Visit) - .ToList(); + : GetOwnReturnStatements(functionDeclaration.Body).Select(Visit).ToList(); return possibleReturnTypes.Count == 0 ? Types.PrimitiveType.Void : TypeSimplifier.Simplify(new Types.UnionType(possibleReturnTypes)); } + private static List GetOwnReturnStatements(Node node) + { + var result = new List(); + CollectOwnReturnStatements(node, result); + return result; + } + + private static void CollectOwnReturnStatements(Node node, List result) + { + foreach (var child in node.Children) + { + if (child is Return returnStatement) + result.Add(returnStatement); + + if (child is not FunctionDeclaration) + CollectOwnReturnStatements(child, result); + } + } + private Type ResolveHoistedType(Symbol symbol) { var type = GetTypeFromSymbol(symbol); diff --git a/Loom.Core/TypeChecking/TypeNarrower.cs b/Loom.Core/TypeChecking/TypeNarrower.cs index a6e8012c..2d6037c9 100644 --- a/Loom.Core/TypeChecking/TypeNarrower.cs +++ b/Loom.Core/TypeChecking/TypeNarrower.cs @@ -4,7 +4,6 @@ using Loom.Core.Parsing.AST; using Loom.Core.Resolving; using Loom.Core.Text; -using Loom.Core.TypeChecking.Types; using LiteralType = Loom.Core.TypeChecking.Types.LiteralType; using PrimitiveType = Loom.Core.TypeChecking.Types.PrimitiveType; using Type = Loom.Core.TypeChecking.Types.Type; diff --git a/Loom.Luau/AST/PropertyAccess.cs b/Loom.Luau/AST/PropertyAccess.cs index 813b7c68..b7b0b57c 100644 --- a/Loom.Luau/AST/PropertyAccess.cs +++ b/Loom.Luau/AST/PropertyAccess.cs @@ -6,6 +6,21 @@ public class PropertyAccess(LuauExpression target, List names) : LuauExp public LuauExpression Target { get; } = target; public List Names { get; } = names; - public override string Render(RenderState state) => - state.ParenthesizeIfNeeded(Target) + (Names.Count > 1 ? '.' : "") + string.Join('.', Names[..^1]) + Operator + Names.Last(); + public override string Render(RenderState state) + { + var result = state.ParenthesizeIfNeeded(Target); + for (var i = 0; i < Names.Count; i++) + { + var name = Names[i]; + if (LuauFactory.Keywords.Contains(name)) + { + result += $"[{RenderState.StringDelimiter}{RenderState.Escape(name)}{RenderState.StringDelimiter}]"; + continue; + } + + result += (i == Names.Count - 1 ? Operator : '.') + name; + } + + return result; + } } \ No newline at end of file diff --git a/Loom.Luau/LuauFactory.cs b/Loom.Luau/LuauFactory.cs index b0ecbf50..657a23a8 100644 --- a/Loom.Luau/LuauFactory.cs +++ b/Loom.Luau/LuauFactory.cs @@ -8,6 +8,13 @@ public static class LuauFactory public static readonly Identifier Self = new("self"); + public static readonly HashSet Keywords = + [ + "and", "break", "do", "else", "elseif", "end", "false", "for", "function", + "if", "in", "local", "nil", "not", "or", "repeat", "return", "then", + "true", "until", "while", "continue" + ]; + public static QualifiedTypeName QualifyRuntimeType(TypeName typeName) => new([RuntimeImportName], typeName); public static Call Bit32Call(string name, List arguments) => LibraryCall("bit32", [name], arguments); diff --git a/Loom.Testing/AstInspectorTest.cs b/Loom.Testing/AstInspectorTest.cs index 8f4cd704..9a4b1d78 100644 --- a/Loom.Testing/AstInspectorTest.cs +++ b/Loom.Testing/AstInspectorTest.cs @@ -158,7 +158,7 @@ public void Inspects_IndexedType() [Fact] public void InspectTree_RendersStatements() { - var lexerResult = new LexerResult(SourceFile.Empty, [], [], new DiagnosticBag()); + var lexerResult = new LexerResult([], [], new DiagnosticBag()); var tree = new Tree(lexerResult, [new InspectorTestStatement { Name = "first" }, new InspectorTestStatement { Name = "second" }]); var inspectionResult = InspectTree(tree); diff --git a/Loom.Testing/CompilationUnitTest.cs b/Loom.Testing/CompilationUnitTest.cs index 6c8f1eb4..b9449186 100644 --- a/Loom.Testing/CompilationUnitTest.cs +++ b/Loom.Testing/CompilationUnitTest.cs @@ -24,8 +24,9 @@ public void Compiles_Project_NoEmit() var path = config.Files.OutputDirectory; Directory.Delete(path, true); Directory.CreateDirectory(path); + File.Create(Path.Combine(path, ".gitkeep")); - var luauFiles = Directory.EnumerateFiles(path); + var luauFiles = Directory.EnumerateFiles(path, "*.luau", SearchOption.TopDirectoryOnly); Assert.Empty(luauFiles); } diff --git a/Loom.Testing/FlowAnalyzerTest.cs b/Loom.Testing/FlowAnalyzerTest.cs index 6a630add..0b0f2d09 100644 --- a/Loom.Testing/FlowAnalyzerTest.cs +++ b/Loom.Testing/FlowAnalyzerTest.cs @@ -149,5 +149,6 @@ public void ThrowsFor_AssignToImmutable(string source) x; """ )] + [InlineData("enum Colors { Red, Green, Blue }; Colors.Red;")] public void Allows(string source) => Utility.AssertNoErrors(Utility.FlowAnalyze(source).AnalyzerResult); } \ No newline at end of file diff --git a/Loom.Testing/LuauGeneratorTest.cs b/Loom.Testing/LuauGeneratorTest.cs index c2791845..a9f7b681 100644 --- a/Loom.Testing/LuauGeneratorTest.cs +++ b/Loom.Testing/LuauGeneratorTest.cs @@ -114,7 +114,7 @@ public void Generates_TypeOfOnPropertyAccess() var luauTree = Utility.GetLuauAST("interface I { a: number } let i = new I { a: 1 }; type X = typeof(i.a);"); var alias = Assert.IsType(luauTree.Statements.Last()); var typeOf = Assert.IsType(alias.Type); - var access = Assert.IsType(typeOf.Expression); + Assert.IsType(typeOf.Expression); Assert.Equal("typeof(i.a)", typeOf.Render(new RenderState())); } diff --git a/Loom.Testing/LuauRenderingTest.cs b/Loom.Testing/LuauRenderingTest.cs index 6f86e62d..8fccf850 100644 --- a/Loom.Testing/LuauRenderingTest.cs +++ b/Loom.Testing/LuauRenderingTest.cs @@ -449,6 +449,27 @@ public void Renders_PropertyAccess_MultipleNames_ColonOperator() Assert.Equal("abc.foo.bar:baz", access.Render()); } + [Fact] + public void Renders_PropertyAccess_KeywordName_AsBracketAccess() + { + var access = new PropertyAccess(new Identifier("obj"), ["end"]); + Assert.Equal("obj[\"end\"]", access.Render()); + } + + [Fact] + public void Renders_PropertyAccess_KeywordName_InMiddleOfChain() + { + var access = new PropertyAccess(new Identifier("obj"), ["a", "repeat", "c"]); + Assert.Equal("obj.a[\"repeat\"].c", access.Render()); + } + + [Fact] + public void Renders_PropertyAccess_NonKeywordName_Unaffected() + { + var access = new PropertyAccess(new Identifier("obj"), ["endpoint"]); + Assert.Equal("obj.endpoint", access.Render()); + } + [Fact] public void Renders_PropertyAccess_ParenthesizesTarget_WhenNeeded() { diff --git a/Loom.Testing/ResolverTest.cs b/Loom.Testing/ResolverTest.cs index b3330880..4761c41f 100644 --- a/Loom.Testing/ResolverTest.cs +++ b/Loom.Testing/ResolverTest.cs @@ -628,7 +628,7 @@ interface Person { var property = symbol.GetPropertyAtPath(["address", "city", "name"]); Assert.NotNull(property); - Assert.Equal("name", property!.Name); + Assert.Equal("name", property.Name); } [Fact] @@ -1439,4 +1439,41 @@ public void Declares_DeclareFunction_InsideBlock() Utility.AssertNoErrors(model); } #endregion Declares + + #region ReservedLuauKeywords + [Theory] + [InlineData("let repeat = 1;", "repeat")] + [InlineData("fn until() {}", "until")] + [InlineData("interface local;", "local")] + public void ThrowsFor_ReservedLuauKeywordDeclaration(string source, string keyword) + { + var diagnostics = Utility.GetSemanticModel(source).Diagnostics; + Utility.AssertDiagnostic(diagnostics, InternalCodes.ReservedLuauKeyword, $"'{keyword}' is a reserved Luau keyword and cannot be used as a declaration name."); + } + + [Fact] + public void ThrowsFor_ReservedLuauKeywordParameter() + { + var diagnostics = Utility.GetSemanticModel("fn foo(local: number): void {}").Diagnostics; + Utility.AssertDiagnostic(diagnostics, InternalCodes.ReservedLuauKeyword, "'local' is a reserved Luau keyword and cannot be used as a declaration name."); + } + + [Theory] + [InlineData("declare let local: number;", "local")] + [InlineData("declare fn end(): void;", "end")] + [InlineData("declare fn f(function: number): void;", "function")] + [InlineData("declare interface repeat;", "repeat")] + public void ThrowsFor_ReservedLuauKeywordName_InAmbientDeclaration(string source, string keyword) + { + var diagnostics = Utility.GetSemanticModel(source).Diagnostics; + Utility.AssertDiagnostic(diagnostics, InternalCodes.ReservedLuauKeyword, $"'{keyword}' is a reserved Luau keyword and cannot be used as a declaration name."); + } + + [Fact] + public void Allows_SelfAsDeclarationName() + { + var model = Utility.GetSemanticModel("fn foo(self: number): void {}"); + Assert.Null(model.Diagnostics.Find(d => d.Code == InternalCodes.ReservedLuauKeyword)); + } + #endregion ReservedLuauKeywords } \ No newline at end of file diff --git a/Loom.Testing/Snapshots/dist/.gitkeep b/Loom.Testing/Snapshots/dist/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/Loom.Testing/SyntaxNodeTest.cs b/Loom.Testing/SyntaxNodeTest.cs index b4c871cf..ed7797f4 100644 --- a/Loom.Testing/SyntaxNodeTest.cs +++ b/Loom.Testing/SyntaxNodeTest.cs @@ -155,7 +155,7 @@ public void FirstAncestorOfType_ReturnsNull_ForTreeItself() { var lit = new Literal(T("42", 0, 2, SyntaxKind.NumberLiteral), 42L); var exprStmt = new ExpressionStatement(lit); - var tree = new Tree(new LexerResult(SourceFile.Empty, [], [], new DiagnosticBag()), [exprStmt]); + var tree = new Tree(new LexerResult([], [], new DiagnosticBag()), [exprStmt]); Assert.Null(tree.FirstAncestorOfType()); Assert.Same(tree, lit.FirstAncestorOfType()); } diff --git a/Loom.Testing/TypeCheckerTest.cs b/Loom.Testing/TypeCheckerTest.cs index 78d345e2..4241dee9 100644 --- a/Loom.Testing/TypeCheckerTest.cs +++ b/Loom.Testing/TypeCheckerTest.cs @@ -438,6 +438,20 @@ public void ThrowsFor_EnumTypeMismatch() Utility.AssertDiagnostic(diagnostics, InternalCodes.TypeMismatch, "Type '5' is not assignable to type '0 | 1'."); } + [Fact] + public void Allows_ReservedLuauKeywordAsEnumMemberName() + { + var diagnostics = Utility.GetTypeCheckerDiagnostics("enum Test { until, A }"); + Assert.Null(diagnostics.Find(d => d.Code == InternalCodes.ReservedLuauKeyword)); + } + + [Fact] + public void Allows_ReservedLuauKeywordAsStringEnumMemberName() + { + var diagnostics = Utility.GetTypeCheckerDiagnostics("enum Test: string { until = \"until\" }"); + Assert.Null(diagnostics.Find(d => d.Code == InternalCodes.ReservedLuauKeyword)); + } + [Fact] public void ThrowsFor_IfStatement_NonBooleanCondition() { diff --git a/Loom.TypeGenerator/Constants.cs b/Loom.TypeGenerator/Constants.cs index 147d0627..6ca91c78 100644 --- a/Loom.TypeGenerator/Constants.cs +++ b/Loom.TypeGenerator/Constants.cs @@ -241,6 +241,7 @@ internal static class Constants { "type", "_type" }, { "interface", "_interface" }, { "old", "oldValue" }, - { "new", "newValue" } + { "new", "newValue" }, + { "function", "callback" } }; } \ No newline at end of file diff --git a/Loom.sln.DotSettings.user b/Loom.sln.DotSettings.user index c674e9d8..09922c3a 100644 --- a/Loom.sln.DotSettings.user +++ b/Loom.sln.DotSettings.user @@ -13,7 +13,8 @@ ForceIncluded ForceIncluded INFO - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Solution /> </SessionState> @@ -27,4 +28,5 @@ + \ No newline at end of file