зеркало из https://github.com/microsoft/Power-Fx.git
Initial support for Power Fx Modules. (#2661)
Only exposed in REPL, and must be explicitly activated. Only new concept in core is adding **FileLocation**. We need to report errors within a file (line,col), not just character offset in an expression. Also implement the Assert() function in the repl - this helps with wrigin modules that act as unit tests. Module support is defined via Repl. Module format is deserialized via YamlDotNet. Modules only support: - a single Formulas section that behaves like app.formulas. - an "imports" section for importing other modules. The tests demonstrate the various combinations for public/private, conflict, shadowing, etc. Add management functions to repl: Import(path) DeleteModule(path) - remove from list ListModules() - show which moduels are loaded
This commit is contained in:
Родитель
ac3d51d049
Коммит
13fa198864
|
@ -44,6 +44,7 @@ using System.Runtime.CompilerServices;
|
|||
[assembly: InternalsVisibleTo("Microsoft.PowerFx.Connectors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.PowerFx.LanguageServerProtocol, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.PowerFx.Json, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.PowerFx.Repl, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.PowerFx.Core.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.PowerFx.Interpreter.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.PowerFx.Json.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
|
||||
|
|
|
@ -39,10 +39,15 @@ namespace Microsoft.PowerFx
|
|||
|
||||
// If this is set directly, it will skip localization.
|
||||
set => _message = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Source location for this error.
|
||||
/// Optional - provide file context for where this expression is from.
|
||||
/// </summary>
|
||||
public FileLocation FragmentLocation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Source location for this error within a single expression.
|
||||
/// </summary>
|
||||
public Span Span { get; set; }
|
||||
|
||||
|
@ -209,6 +214,24 @@ namespace Microsoft.PowerFx
|
|||
return errors.Select(x => ExpressionError.New(x, locale)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
// Translate Span in original text (start,end) to something more useful for a file.
|
||||
internal static IEnumerable<ExpressionError> NewFragment(IEnumerable<IDocumentError> errors, string originalText, FileLocation fragmentLocation)
|
||||
{
|
||||
if (errors == null)
|
||||
{
|
||||
return Array.Empty<ExpressionError>();
|
||||
}
|
||||
else
|
||||
{
|
||||
return errors.Select(x =>
|
||||
{
|
||||
var error = ExpressionError.New(x, null);
|
||||
error.FragmentLocation = fragmentLocation.Apply(originalText, error.Span);
|
||||
return error;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.PowerFx.Core.Errors;
|
||||
using Microsoft.PowerFx.Core.Localization;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
namespace Microsoft.PowerFx.Syntax
|
||||
{
|
||||
/// <summary>
|
||||
/// File-aware, Multi-line source span with Line and Column.
|
||||
/// Wheras <see cref="Span"/> is a character span within a single expression.
|
||||
/// </summary>
|
||||
public class FileLocation
|
||||
{
|
||||
public string Filename { get; init; }
|
||||
|
||||
// 1-based index
|
||||
public int LineStart { get; init; }
|
||||
|
||||
// 1-based index.
|
||||
public int ColStart { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Apply a span (which is a character offset within a single expression)
|
||||
/// to this file offset.
|
||||
/// </summary>
|
||||
/// <param name="originalText"></param>
|
||||
/// <param name="s2"></param>
|
||||
/// <returns></returns>
|
||||
public FileLocation Apply(string originalText, Span s2)
|
||||
{
|
||||
// Convert index span to line span.
|
||||
int iLine = this.LineStart;
|
||||
int iCol = ColStart;
|
||||
|
||||
for (int i = 0; i < s2.Min; i++)
|
||||
{
|
||||
if (originalText[i] == '\r')
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
else if (originalText[i] == '\n')
|
||||
{
|
||||
iLine++;
|
||||
iCol = ColStart; // reset
|
||||
}
|
||||
else
|
||||
{
|
||||
iCol++;
|
||||
}
|
||||
}
|
||||
|
||||
return new FileLocation
|
||||
{
|
||||
Filename = Filename,
|
||||
ColStart = iCol,
|
||||
LineStart = iLine
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.PowerFx.Repl.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")]
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
|
||||
namespace Microsoft.PowerFx.Repl.Functions
|
||||
{
|
||||
/// <summary>
|
||||
/// Delete a modules.
|
||||
/// </summary>
|
||||
internal class DeleteModuleFunction : ReflectionFunction
|
||||
{
|
||||
private readonly PowerFxREPL _repl;
|
||||
|
||||
public DeleteModuleFunction(PowerFxREPL repl)
|
||||
: base("DeleteModule", FormulaType.Void, new[] { FormulaType.String })
|
||||
{
|
||||
ConfigType = typeof(IReplOutput);
|
||||
_repl = repl;
|
||||
}
|
||||
|
||||
public async Task<VoidValue> Execute(IReplOutput output, StringValue value, CancellationToken cancel)
|
||||
{
|
||||
if (!_repl.TryResolveModule(value.Value, out var module))
|
||||
{
|
||||
await output.WriteLineAsync($"Can't resolve module '{value.Value}'. Try ListModules() to see loaded modules.", OutputKind.Error, cancel)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
_repl.DeleteModule(module);
|
||||
|
||||
await output.WriteLineAsync("Removed module: " + module.FullPath, OutputKind.Notify, cancel)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return FormulaValue.NewVoid();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
|
||||
namespace Microsoft.PowerFx.Repl.Functions
|
||||
{
|
||||
/// <summary>
|
||||
/// Import a module.
|
||||
/// </summary>
|
||||
internal class ImportFunction : ReflectionFunction
|
||||
{
|
||||
private readonly PowerFxREPL _repl;
|
||||
|
||||
public ImportFunction(PowerFxREPL repl)
|
||||
: base("Import", FormulaType.Void, new[] { FormulaType.String })
|
||||
{
|
||||
ConfigType = typeof(IReplOutput);
|
||||
_repl = repl;
|
||||
}
|
||||
|
||||
public async Task<VoidValue> Execute(IReplOutput output, StringValue name, CancellationToken cancel)
|
||||
{
|
||||
var errors = new List<ExpressionError>();
|
||||
|
||||
var ctx = new ModuleLoadContext(_repl.Engine.GetCombinedEngineSymbols());
|
||||
|
||||
string filename = name.Value;
|
||||
var module = await ctx.LoadFromFileAsync(filename, errors).ConfigureAwait(false);
|
||||
|
||||
foreach (var error in errors)
|
||||
{
|
||||
// Adjust to file...?
|
||||
var loc = error.FragmentLocation;
|
||||
|
||||
var shortName = Path.GetFileName(loc.Filename);
|
||||
|
||||
var prefix = error.IsWarning ? "Warning" : "Error";
|
||||
var kind = error.IsWarning ? OutputKind.Warning : OutputKind.Error;
|
||||
|
||||
var msg = $"{prefix}: {shortName} ({loc.LineStart},{loc.ColStart}): {error.Message}";
|
||||
|
||||
await output.WriteLineAsync(msg, kind, cancel)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var hasErrors = errors.Where(error => !error.IsWarning).Any();
|
||||
|
||||
if (!hasErrors)
|
||||
{
|
||||
// Apply these functions to engine.
|
||||
|
||||
string header = "Defined functions:";
|
||||
await output.WriteLineAsync(header, OutputKind.Notify, cancel)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await PrintModuleAsync(module, output, cancel)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
_repl.AddModule(module);
|
||||
}
|
||||
|
||||
return FormulaValue.NewVoid();
|
||||
}
|
||||
|
||||
internal static async Task PrintModuleAsync(Module module, IReplOutput output, CancellationToken cancel)
|
||||
{
|
||||
foreach (var funcName in module.Symbols.FunctionNames)
|
||||
{
|
||||
await output.WriteLineAsync($" {funcName}", OutputKind.Notify, cancel)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await output.WriteLineAsync(string.Empty, OutputKind.Notify, cancel)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
|
||||
namespace Microsoft.PowerFx.Repl.Functions
|
||||
{
|
||||
/// <summary>
|
||||
/// List the modules that are loaded.
|
||||
/// </summary>
|
||||
internal class ListModulesFunction : ReflectionFunction
|
||||
{
|
||||
private readonly PowerFxREPL _repl;
|
||||
|
||||
public ListModulesFunction(PowerFxREPL repl)
|
||||
: base("ListModules", FormulaType.Void)
|
||||
{
|
||||
ConfigType = typeof(IReplOutput);
|
||||
_repl = repl;
|
||||
}
|
||||
|
||||
public async Task<VoidValue> Execute(IReplOutput output, CancellationToken cancel)
|
||||
{
|
||||
var modules = _repl.Modules;
|
||||
|
||||
await output.WriteLineAsync("Modules loaded:", OutputKind.Notify, cancel)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var module in modules)
|
||||
{
|
||||
await output.WriteLineAsync(module.FullPath, OutputKind.Notify, cancel)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
await ImportFunction.PrintModuleAsync(module, output, cancel)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return FormulaValue.NewVoid();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@
|
|||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.PowerFx.Core\Microsoft.PowerFx.Core.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.PowerFx.Interpreter\Microsoft.PowerFx.Interpreter.csproj" />
|
||||
<PackageReference Include="YamlDotNet" Version="13.4.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl
|
||||
{
|
||||
// Ensure uniqueness
|
||||
internal class ConflictTracker
|
||||
{
|
||||
// Map from symbol Name to the module it was defined in.
|
||||
private readonly Dictionary<string, Module> _defined = new Dictionary<string, Module>();
|
||||
|
||||
/// <summary>
|
||||
/// Verify that the symbols added by Module are unique.
|
||||
/// </summary>
|
||||
/// <param name="module"></param>
|
||||
/// <exception cref="InvalidOperationException">If a symbol is already defined.</exception>
|
||||
public void VerifyUnique(Module module)
|
||||
{
|
||||
foreach (var name in module.Symbols.FunctionNames)
|
||||
{
|
||||
if (_defined.TryGetValue(name, out var original))
|
||||
{
|
||||
throw new InvalidOperationException($"Symbol '{name}' is already defined in both '{original.FullPath}' and '{module.FullPath}'");
|
||||
}
|
||||
|
||||
_defined[name] = module;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Core.Binding;
|
||||
using Microsoft.PowerFx.Core.Errors;
|
||||
using Microsoft.PowerFx.Core.Functions;
|
||||
using Microsoft.PowerFx.Core.Glue;
|
||||
using Microsoft.PowerFx.Core.Utils;
|
||||
using Microsoft.PowerFx.Syntax;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Core.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NodeDeserializers;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl
|
||||
{
|
||||
internal class FileLoader : IFileLoader
|
||||
{
|
||||
// Root directory that we load from.
|
||||
private readonly string _root;
|
||||
|
||||
public FileLoader(string root)
|
||||
{
|
||||
if (!Path.IsPathRooted(root))
|
||||
{
|
||||
throw new ArgumentException($"Path must be rooted: {root}", nameof(root));
|
||||
}
|
||||
|
||||
_root = root;
|
||||
}
|
||||
|
||||
public async Task<(ModulePoco, IFileLoader)> LoadAsync(string name)
|
||||
{
|
||||
name = name.Trim();
|
||||
var fullPath = Path.Combine(_root, name);
|
||||
|
||||
var txt = File.ReadAllText(fullPath);
|
||||
|
||||
// Deserialize.
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithTypeConverter(new StringWithSourceConverter(fullPath, txt))
|
||||
.Build();
|
||||
var modulePoco = deserializer.Deserialize<ModulePoco>(txt);
|
||||
|
||||
modulePoco.Src_Filename = fullPath;
|
||||
|
||||
return (modulePoco, this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl
|
||||
{
|
||||
/// <summary>
|
||||
/// A load can resolve a name to Module contents.
|
||||
/// </summary>
|
||||
internal interface IFileLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Load a module poco from the given fulename.
|
||||
/// </summary>
|
||||
/// <param name="filename">a filename. Could be full path or relative. The loader will resolve.</param>
|
||||
/// <returns>The loaded module contents and a new file loader for resolving any subsequent imports in this module.</returns>
|
||||
Task<(ModulePoco, IFileLoader)> LoadAsync(string filename);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl
|
||||
{
|
||||
internal class Module
|
||||
{
|
||||
/// <summary>
|
||||
/// Public symbols exported by this module.
|
||||
/// </summary>
|
||||
public ReadOnlySymbolTable Symbols { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Identity of the module. We should never have two different modules with the same identity.
|
||||
/// </summary>
|
||||
public ModuleIdentity Identity { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional Full path that this module was loaded from. Null if not loaded from a file.
|
||||
/// Primarily useful for helping makers debugging ("where did this module come from").
|
||||
/// </summary>
|
||||
public string FullPath { get; init; }
|
||||
|
||||
internal Module(ModuleIdentity identity, ReadOnlySymbolTable exports)
|
||||
{
|
||||
this.Identity = identity;
|
||||
this.Symbols = exports ?? throw new ArgumentNullException(nameof(exports));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis.VisualBasic.Syntax;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl
|
||||
{
|
||||
/// <summary>
|
||||
/// A comparable handle representing module identity.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{_value}")]
|
||||
internal struct ModuleIdentity
|
||||
{
|
||||
private readonly string _value;
|
||||
|
||||
private ModuleIdentity(string value)
|
||||
{
|
||||
_value = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an identify for a file.
|
||||
/// </summary>
|
||||
/// <param name="fullPath">full path to file. File does not need to actually exist.</param>
|
||||
/// <returns>An identity.</returns>
|
||||
public static ModuleIdentity FromFile(string fullPath)
|
||||
{
|
||||
if (fullPath == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(fullPath));
|
||||
}
|
||||
|
||||
if (!Path.IsPathRooted(fullPath))
|
||||
{
|
||||
throw new ArgumentException("Path must be rooted", nameof(fullPath));
|
||||
}
|
||||
|
||||
// GetFullPath will normalize ".." to a canonical representation.
|
||||
// Path does not need to actually exist.
|
||||
string canonicalPath = Path.GetFullPath(fullPath);
|
||||
|
||||
// Lower to avoid case sensitivity.
|
||||
var value = canonicalPath.ToLowerInvariant();
|
||||
|
||||
return new ModuleIdentity(value);
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is ModuleIdentity other)
|
||||
{
|
||||
return _value == other._value;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return _value == null ? 0 : _value.GetHashCode();
|
||||
}
|
||||
|
||||
// Do not implement ToString - identity should be treated opaque.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Core.Binding;
|
||||
using Microsoft.PowerFx.Core.Errors;
|
||||
using Microsoft.PowerFx.Core.Functions;
|
||||
using Microsoft.PowerFx.Core.Glue;
|
||||
using Microsoft.PowerFx.Core.Types;
|
||||
using Microsoft.PowerFx.Syntax;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl
|
||||
{
|
||||
internal static class ModuleLoadContextExtentions
|
||||
{
|
||||
// Load a module from disk.
|
||||
// This may throw on hard error (missing file), or return null with compile errors.
|
||||
public static async Task<Module> LoadFromFileAsync(this ModuleLoadContext context, string path, List<ExpressionError> errors)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
var name = Path.GetFileName(path);
|
||||
|
||||
var loader = new FileLoader(dir);
|
||||
|
||||
return await context.LoadAsync(name, loader, errors).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context for loading a single module.
|
||||
/// This can ensure the module dependencies don't have cycles.
|
||||
/// </summary>
|
||||
internal class ModuleLoadContext
|
||||
{
|
||||
// Core symbols we resolve against.
|
||||
// This is needed for all the builtins.
|
||||
private readonly ReadOnlySymbolTable _commonIncomingSymbols;
|
||||
|
||||
// Set of modules we visited - used to detect cycles.
|
||||
// Key String is the module's full path.
|
||||
// Value is null if module is in-progress of being loaded (this detects cycles).
|
||||
// We still allow diamond dependencies.
|
||||
private readonly Dictionary<ModuleIdentity, Module> _alreadyLoaded = new Dictionary<ModuleIdentity, Module>();
|
||||
|
||||
public ModuleLoadContext(ReadOnlySymbolTable commonIncomingSymbols)
|
||||
{
|
||||
_commonIncomingSymbols = commonIncomingSymbols;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load and return a resolved module. May throw on errors.
|
||||
/// </summary>
|
||||
/// <param name="name">name to resolve via loader parameter.</param>
|
||||
/// <param name="loader">file loader to resolve the name and any further imports.</param>
|
||||
/// <param name="errors">error collection to populate if there are errors in parsing, binding, etc. </param>
|
||||
/// <returns>A module. Or possible null if there are errors. Or may throw. </returns>
|
||||
public async Task<Module> LoadAsync(string name, IFileLoader loader, List<ExpressionError> errors)
|
||||
{
|
||||
(var poco, var loader2) = await loader.LoadAsync(name).ConfigureAwait(false);
|
||||
|
||||
ModuleIdentity fullPathIdentity = poco.GetIdentity();
|
||||
if (_alreadyLoaded.TryGetValue(fullPathIdentity, out var existing))
|
||||
{
|
||||
if (existing != null)
|
||||
{
|
||||
// If we fully loaded the module, then it's not circular.
|
||||
return existing;
|
||||
}
|
||||
|
||||
// Circular reference.
|
||||
throw new InvalidOperationException($"Circular reference: {name}");
|
||||
}
|
||||
|
||||
_alreadyLoaded.Add(fullPathIdentity, null); // In-progress
|
||||
|
||||
// Load all imports first.
|
||||
|
||||
var symbolList = new List<ReadOnlySymbolTable>
|
||||
{
|
||||
_commonIncomingSymbols
|
||||
};
|
||||
var unique = new ConflictTracker();
|
||||
|
||||
if (poco.Imports != null)
|
||||
{
|
||||
foreach (var import in poco.Imports)
|
||||
{
|
||||
string file = import.File;
|
||||
if (file != null)
|
||||
{
|
||||
var m2 = await LoadAsync(file, loader2, errors).ConfigureAwait(false);
|
||||
if (m2 == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
unique.VerifyUnique(m2);
|
||||
symbolList.Add(m2.Symbols);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var incomingSymbols = ReadOnlySymbolTable.Compose(symbolList.ToArray());
|
||||
|
||||
var moduleExports = new SymbolTable { DebugName = name };
|
||||
|
||||
bool ok = ResolveBody(poco, moduleExports, incomingSymbols, errors);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
// On errors
|
||||
return null;
|
||||
}
|
||||
|
||||
var module = new Module(fullPathIdentity, moduleExports)
|
||||
{
|
||||
FullPath = poco.Src_Filename
|
||||
};
|
||||
|
||||
_alreadyLoaded[fullPathIdentity] = module; // done loading.
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
// poco - the yaml file contents.
|
||||
// incomingSymbols - what body can bind to. Builtins, etc.
|
||||
// any module dependencies should already be resolved and included here.
|
||||
// moduleExports - symbols to add to.
|
||||
// Return true on successful load, false on failure
|
||||
private bool ResolveBody(ModulePoco poco, SymbolTable moduleExports, ReadOnlySymbolTable incomingSymbols, List<ExpressionError> errors)
|
||||
{
|
||||
var str = poco.Formulas.Value;
|
||||
|
||||
bool allowSideEffects = true;
|
||||
var options = new ParserOptions
|
||||
{
|
||||
AllowParseAsTypeLiteral = true,
|
||||
AllowsSideEffects = true
|
||||
};
|
||||
|
||||
var parseResult = UserDefinitions.Parse(str, options);
|
||||
|
||||
var fragmentLocation = poco.Formulas.Location;
|
||||
|
||||
// Scan for errors.
|
||||
errors.AddRange(ExpressionError.NewFragment(parseResult.Errors, str, fragmentLocation));
|
||||
|
||||
if (errors.Any(x => !x.IsWarning))
|
||||
{
|
||||
// Abort on errors.
|
||||
return false;
|
||||
}
|
||||
|
||||
{
|
||||
var errors2 = parseResult.UDFs.Where(udf => !udf.IsParseValid).FirstOrDefault();
|
||||
if (errors2 != null)
|
||||
{
|
||||
// Should have been caught above.
|
||||
throw new InvalidOperationException($"Errors should have already been caught");
|
||||
}
|
||||
}
|
||||
|
||||
var definedTypes = parseResult.DefinedTypes;
|
||||
if (definedTypes != null && definedTypes.Any())
|
||||
{
|
||||
var resolvedTypes = DefinedTypeResolver.ResolveTypes(definedTypes, incomingSymbols, out var errors4);
|
||||
errors.AddRange(ExpressionError.NewFragment(errors4, str, fragmentLocation));
|
||||
if (errors.Any(x => !x.IsWarning))
|
||||
{
|
||||
// Abort on errors.
|
||||
return false;
|
||||
}
|
||||
|
||||
moduleExports.AddTypes(resolvedTypes);
|
||||
}
|
||||
|
||||
var s2 = ReadOnlySymbolTable.Compose(moduleExports, incomingSymbols);
|
||||
|
||||
// Convert parse --> TexlFunctions
|
||||
// fail if duplicates detected within this batch.
|
||||
// fail if any names are resverd
|
||||
// Basic type checking
|
||||
IEnumerable<UserDefinedFunction> udfs = UserDefinedFunction.CreateFunctions(parseResult.UDFs, s2, out var errors3);
|
||||
|
||||
errors.AddRange(ExpressionError.NewFragment(errors3, str, fragmentLocation));
|
||||
if (errors.Any(x => !x.IsWarning))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Body can refer to other functions defined in this batch. So we need 2 pass.
|
||||
// First add all definitions.
|
||||
foreach (var udf in udfs)
|
||||
{
|
||||
moduleExports.AddFunction(udf);
|
||||
}
|
||||
|
||||
// Then bind all bodies.
|
||||
foreach (var udf in udfs)
|
||||
{
|
||||
var config = new BindingConfig(allowsSideEffects: allowSideEffects, useThisRecordForRuleScope: false, numberIsFloat: false);
|
||||
|
||||
Features features = Features.PowerFxV1;
|
||||
|
||||
var binding = udf.BindBody(s2, new Glue2DocumentBinderGlue(), config, features);
|
||||
|
||||
List<TexlError> bindErrors = new List<TexlError>();
|
||||
|
||||
binding.ErrorContainer.GetErrors(ref bindErrors);
|
||||
errors.AddRange(ExpressionError.NewFragment(bindErrors, str, fragmentLocation));
|
||||
|
||||
if (errors.Any(x => !x.IsWarning))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.PowerFx.Syntax;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl
|
||||
{
|
||||
/// <summary>
|
||||
/// The yaml representation of a module.
|
||||
/// Properties with 'Src_' prefix are set by deserializer and not part of the yaml file contents.
|
||||
/// </summary>
|
||||
internal class ModulePoco
|
||||
{
|
||||
/// <summary>
|
||||
/// Full path of file this was loaded from.
|
||||
/// This is set by the deserializer.
|
||||
/// </summary>
|
||||
public string Src_Filename { get; set; }
|
||||
|
||||
public ModuleIdentity GetIdentity()
|
||||
{
|
||||
return ModuleIdentity.FromFile(Src_Filename);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contain Power Fx UDF declarations.
|
||||
/// </summary>
|
||||
public StringWithSource Formulas { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set of modules that this depends on.
|
||||
/// </summary>
|
||||
public ImportPoco[] Imports { get; set; }
|
||||
}
|
||||
|
||||
internal class ImportPoco
|
||||
{
|
||||
/// <summary>
|
||||
/// File path to import from. "foo.fx.yml" or ".\path\foo.fx.yml".
|
||||
/// </summary>
|
||||
public string File { get; set; }
|
||||
|
||||
// identifier resolved against the host.
|
||||
public string Host { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.PowerFx.Syntax;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a string object, but also tagged with a solution location for where the string contents start.
|
||||
/// This is restircted to Inline strings (and rejects other formats like fodling, quotes, etc).
|
||||
/// Use a custom converter to populate the source location of the string. See <see cref="StringWithSourceConverter"/> .
|
||||
/// </summary>
|
||||
internal class StringWithSource
|
||||
{
|
||||
/// <summary>
|
||||
/// The contents of the string from the yaml.
|
||||
/// </summary>
|
||||
public string Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The location in the file for where the contents start. Useful for error reporting.
|
||||
/// </summary>
|
||||
public FileLocation Location { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Core.Binding;
|
||||
using Microsoft.PowerFx.Core.Errors;
|
||||
using Microsoft.PowerFx.Core.Functions;
|
||||
using Microsoft.PowerFx.Core.Glue;
|
||||
using Microsoft.PowerFx.Core.Utils;
|
||||
using Microsoft.PowerFx.Syntax;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Core.Events;
|
||||
using YamlDotNet.Serialization;
|
||||
using YamlDotNet.Serialization.NodeDeserializers;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl
|
||||
{
|
||||
/// <summary>
|
||||
/// Yaml converter to parse a <see cref="StringWithSource"/> and tag
|
||||
/// it with the source location.
|
||||
/// </summary>
|
||||
internal class StringWithSourceConverter : IYamlTypeConverter
|
||||
{
|
||||
private readonly string _filename;
|
||||
private readonly string[] _lines; // to infer indent level.
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StringWithSourceConverter"/> class.
|
||||
/// </summary>
|
||||
/// <param name="filename">filename to tag with.</param>
|
||||
/// <param name="contents">contents of the file - this is used to infer indenting depth that can't be gotten from th yaml parser.</param>
|
||||
public StringWithSourceConverter(string filename, string contents)
|
||||
{
|
||||
_filename = filename;
|
||||
|
||||
_lines = contents.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
|
||||
}
|
||||
|
||||
public bool Accepts(Type type)
|
||||
{
|
||||
return type == typeof(StringWithSource);
|
||||
}
|
||||
|
||||
public object ReadYaml(IParser parser, Type type)
|
||||
{
|
||||
var val = parser.Consume<Scalar>();
|
||||
|
||||
// Empty case
|
||||
if (val.Style == ScalarStyle.Plain &&
|
||||
val.Value.Length == 0)
|
||||
{
|
||||
return new StringWithSource
|
||||
{
|
||||
Value = null, // Preserve same semantics as String deserialization.
|
||||
Location = new FileLocation
|
||||
{
|
||||
Filename = _filename,
|
||||
LineStart = val.Start.Line,
|
||||
ColStart = val.Start.Column
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/3790454/how-do-i-break-a-string-in-yaml-over-multiple-lines
|
||||
// Yaml has many multi-line encodings. Fortunately, we can determine this based on ScalarStyle
|
||||
// We want to force | , which is Literal. This means the fx is copy & paste into yaml, with an indent level
|
||||
// and no other mutation.
|
||||
//
|
||||
// This will prevent all other forms, including:
|
||||
// > is "fold" which will remove newlines.
|
||||
// "" is double quotes.
|
||||
// plain where the string is single-line embedded.
|
||||
|
||||
if (val.Style != ScalarStyle.Literal)
|
||||
{
|
||||
// This is important if we want to preserve character index mapping.
|
||||
throw new InvalidOperationException($"must be literal string encoding at {val.Start.Line}");
|
||||
}
|
||||
|
||||
int line1PropertyName = val.Start.Line; // 1-based Line that the "Formulas" property is on. Content starts on next line.
|
||||
|
||||
// YamlParser does not tell us indent level. Must discern it.
|
||||
var line = _lines[line1PropertyName];
|
||||
var colStart0 = line.FindIndex(x => !char.IsWhiteSpace(x));
|
||||
|
||||
return new StringWithSource
|
||||
{
|
||||
Value = val.Value,
|
||||
Location = new FileLocation
|
||||
{
|
||||
Filename = _filename,
|
||||
LineStart = line1PropertyName + 1,
|
||||
ColStart = colStart0 + 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void WriteYaml(IEmitter emitter, object value, Type type)
|
||||
{
|
||||
// Only reads.
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
} // end class
|
||||
}
|
|
@ -37,6 +37,13 @@ namespace Microsoft.PowerFx
|
|||
// Allow repl to create new UserDefinedFunctions.
|
||||
public bool AllowUserDefinedFunctions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable the Import() function for importing modules.
|
||||
/// Defaults to false.
|
||||
/// </summary>
|
||||
[Obsolete("preview")]
|
||||
public bool AllowImport { get; set; }
|
||||
|
||||
// Do we print each command before evaluation?
|
||||
// Useful if we're running a file and are debugging, or if input UI is separated from output UI.
|
||||
public bool Echo { get; set; } = false;
|
||||
|
@ -76,6 +83,11 @@ namespace Microsoft.PowerFx
|
|||
/// </summary>
|
||||
public ReadOnlySymbolValues ExtraSymbolValues { get; set; }
|
||||
|
||||
// Map from Module full path to Module.
|
||||
private readonly Dictionary<string, Module> _loadedModules = new Dictionary<string, Module>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
internal IEnumerable<Module> Modules => _loadedModules.Values;
|
||||
|
||||
/// <summary>
|
||||
/// Get sorted names of all functions. This includes functions from the <see cref="Engine"/> as well as <see cref="MetaFunctions"/>.
|
||||
/// </summary>
|
||||
|
@ -94,6 +106,68 @@ namespace Microsoft.PowerFx
|
|||
}
|
||||
}
|
||||
|
||||
// Get combined symbol values (extra, modules, etc)
|
||||
public ReadOnlySymbolValues GetCombined()
|
||||
{
|
||||
// Combine with symbols from modules.
|
||||
var list = new List<ReadOnlySymbolTable>();
|
||||
|
||||
foreach (var module in _loadedModules.Values)
|
||||
{
|
||||
list.Add(module.Symbols);
|
||||
}
|
||||
|
||||
if (this.ExtraSymbolValues != null)
|
||||
{
|
||||
list.Add(this.ExtraSymbolValues.SymbolTable);
|
||||
}
|
||||
|
||||
var m1 = ReadOnlySymbolTable.Compose(list.ToArray());
|
||||
|
||||
var values = m1.CreateValues(this.ExtraSymbolValues);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
internal bool TryResolveModule(string path, out Module module)
|
||||
{
|
||||
if (_loadedModules.TryGetValue(path, out module))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// can we resolve by short name?
|
||||
List<Module> list = new List<Module>();
|
||||
|
||||
foreach (var m in _loadedModules.Values)
|
||||
{
|
||||
string shortName = System.IO.Path.GetFileName(m.FullPath);
|
||||
if (string.Equals(shortName, path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
list.Add(m);
|
||||
}
|
||||
}
|
||||
|
||||
if (list.Count == 1)
|
||||
{
|
||||
module = list[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal void DeleteModule(Module module)
|
||||
{
|
||||
_loadedModules.Remove(module.FullPath);
|
||||
}
|
||||
|
||||
internal void AddModule(Module module)
|
||||
{
|
||||
string id = module.FullPath;
|
||||
_loadedModules[id] = module;
|
||||
}
|
||||
|
||||
// Interpreter should normally not throw.
|
||||
// Exceptions should be caught and converted to ErrorResult.
|
||||
// Not called for OperationCanceledException since those are expected.
|
||||
|
@ -123,6 +197,8 @@ namespace Microsoft.PowerFx
|
|||
this.MetaFunctions.AddFunction(new Help1Function(this));
|
||||
}
|
||||
|
||||
private bool _finishInit = false;
|
||||
|
||||
private bool _userEnabled = false;
|
||||
|
||||
public void EnableSampleUserObject()
|
||||
|
@ -217,6 +293,22 @@ namespace Microsoft.PowerFx
|
|||
}
|
||||
}
|
||||
|
||||
// Property ctor is run before Init properties are set.
|
||||
// So apply final initialization after property initializers but before we execute commands.
|
||||
private void FinishInit()
|
||||
{
|
||||
if (!_finishInit)
|
||||
{
|
||||
_finishInit = true;
|
||||
if (this.AllowImport)
|
||||
{
|
||||
this.MetaFunctions.AddFunction(new ImportFunction(this));
|
||||
this.MetaFunctions.AddFunction(new ListModulesFunction(this));
|
||||
this.MetaFunctions.AddFunction(new DeleteModuleFunction(this));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Directly invoke a command. This skips multiline handling.
|
||||
/// </summary>
|
||||
|
@ -239,6 +331,8 @@ namespace Microsoft.PowerFx
|
|||
return new ReplResult();
|
||||
}
|
||||
|
||||
FinishInit();
|
||||
|
||||
if (this.Echo)
|
||||
{
|
||||
await this.WritePromptAsync(cancel);
|
||||
|
@ -247,9 +341,10 @@ namespace Microsoft.PowerFx
|
|||
await this.Output.WriteLineAsync(expression.TrimEnd(), OutputKind.Repl, cancel);
|
||||
}
|
||||
|
||||
var extraSymbolTable = this.ExtraSymbolValues?.SymbolTable;
|
||||
var extraValues = this.GetCombined();
|
||||
var extraSymbolTable = extraValues.SymbolTable;
|
||||
|
||||
var runtimeConfig = new RuntimeConfig(this.ExtraSymbolValues)
|
||||
var runtimeConfig = new RuntimeConfig(extraValues)
|
||||
{
|
||||
ServiceProvider = new BasicServiceProvider(this.InnerServices)
|
||||
};
|
||||
|
@ -363,7 +458,7 @@ namespace Microsoft.PowerFx
|
|||
// Get the type.
|
||||
var rhsExpr = declare._rhs.GetCompleteSpan().GetFragment(expression);
|
||||
|
||||
var setCheck = this.Engine.Check(rhsExpr, ParserOptions, this.ExtraSymbolValues?.SymbolTable);
|
||||
var setCheck = this.Engine.Check(rhsExpr, ParserOptions, this.GetCombined().SymbolTable);
|
||||
if (!setCheck.IsSuccess)
|
||||
{
|
||||
await this.Output.WriteLineAsync($"Error: Failed to initialize '{name}'.", OutputKind.Error, cancel)
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.PowerFx.Core.Errors;
|
||||
using Microsoft.PowerFx.Core.Localization;
|
||||
using Microsoft.PowerFx.Syntax;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.PowerFx.Core.Tests
|
||||
{
|
||||
public class FileLocationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0, 200, 300)]
|
||||
[InlineData(1, 200, 301)]
|
||||
[InlineData(4, 201, 300)]
|
||||
[InlineData(9, 202, 301)]
|
||||
public void Apply(int min, int expectedLine, int expectedCol)
|
||||
{
|
||||
string file =
|
||||
|
||||
// 012 345 6 789
|
||||
"ABC\nDE\r\nFG";
|
||||
|
||||
var loc = new FileLocation
|
||||
{
|
||||
Filename = "test",
|
||||
LineStart = 200,
|
||||
ColStart = 300
|
||||
};
|
||||
|
||||
var span = new Span(min, min + 1);
|
||||
var loc2 = loc.Apply(file, span);
|
||||
|
||||
Assert.Same(loc.Filename, loc2.Filename);
|
||||
Assert.Equal(expectedLine, loc2.LineStart);
|
||||
Assert.Equal(expectedCol, loc2.ColStart);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -68,6 +68,7 @@ namespace Microsoft.PowerFx.Core.Tests
|
|||
"Microsoft.PowerFx.Syntax.IdentToken",
|
||||
"Microsoft.PowerFx.Syntax.NumLitToken",
|
||||
"Microsoft.PowerFx.Syntax.Span",
|
||||
"Microsoft.PowerFx.Syntax.FileLocation",
|
||||
"Microsoft.PowerFx.Syntax.StrLitToken",
|
||||
"Microsoft.PowerFx.Syntax.Token",
|
||||
"Microsoft.PowerFx.Syntax.TokKind",
|
||||
|
|
|
@ -10,8 +10,20 @@
|
|||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="$(MSBuildThisFileDirectory)AssemblyProperties2.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Mocks\TempFileHolder.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ModuleIdentityTests.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ModuleTests.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)MultilineProcessorTests.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)ReplTests.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)Mocks\TestReplOutput.cs" />
|
||||
<Compile Include="$(MSBuildThisFileDirectory)StringWithSourceTests.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="$(MSBuildThisFileDirectory)Modules\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="$(MSBuildThisFileDirectory)modules\*.fx.yml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,27 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl.Tests
|
||||
{
|
||||
// Helper for creating temp files and cleaning up.
|
||||
internal class TempFileHolder : IDisposable
|
||||
{
|
||||
public string FullPath { get; } = Path.GetTempFileName();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup the file.
|
||||
try
|
||||
{
|
||||
File.Delete(this.FullPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl.Tests
|
||||
{
|
||||
public class ModuleIdentityTests
|
||||
{
|
||||
[Fact]
|
||||
public void Collections()
|
||||
{
|
||||
var id1a = ModuleIdentity.FromFile(@"z:\path1.txt");
|
||||
var id1b = ModuleIdentity.FromFile(@"z:\Path1.txt");
|
||||
|
||||
var id2 = ModuleIdentity.FromFile(@"z:\path2.txt");
|
||||
|
||||
Assert.Equal(id1a, id1a); // identity
|
||||
Assert.Equal(id1a.GetHashCode(), id1a.GetHashCode());
|
||||
|
||||
Assert.NotEqual(id1a, id2);
|
||||
|
||||
// paths should be normalized
|
||||
Assert.Equal(id1a, id1b);
|
||||
|
||||
// Work with collections.
|
||||
var set = new HashSet<ModuleIdentity>();
|
||||
|
||||
var added = set.Add(id1a);
|
||||
Assert.True(added);
|
||||
|
||||
added = set.Add(id1a);
|
||||
Assert.False(added);
|
||||
|
||||
added = set.Add(id1b);
|
||||
Assert.False(added);
|
||||
|
||||
Assert.Contains(id1a, set);
|
||||
Assert.Contains(id1b, set);
|
||||
|
||||
Assert.DoesNotContain(id2, set);
|
||||
|
||||
added = set.Add(id2);
|
||||
Assert.True(added);
|
||||
}
|
||||
|
||||
// Ensure we normalize file paths, particularly when using ".."
|
||||
[Fact]
|
||||
public void Canonical()
|
||||
{
|
||||
var id1a = ModuleIdentity.FromFile(@"z:\foo\path1.txt");
|
||||
var id1b = ModuleIdentity.FromFile(@"z:\foo\bar\..\Path1.txt");
|
||||
|
||||
Assert.Equal(id1a, id1a); // identity
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoToString()
|
||||
{
|
||||
var id1 = ModuleIdentity.FromFile(@"z:\foo\path1.txt");
|
||||
|
||||
// *Don't implement ToString* - we want to treat identity as opauque.
|
||||
var str = id1.ToString();
|
||||
|
||||
Assert.Equal("Microsoft.PowerFx.Repl.ModuleIdentity", str);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MustBeFullPath()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => ModuleIdentity.FromFile(null));
|
||||
|
||||
// Must be a full path
|
||||
Assert.Throws<ArgumentException>(() => ModuleIdentity.FromFile("path1.txt"));
|
||||
Assert.Throws<ArgumentException>(() => ModuleIdentity.FromFile(@".\path1.txt"));
|
||||
Assert.Throws<ArgumentException>(() => ModuleIdentity.FromFile(string.Empty));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.VisualBasic.Syntax;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
#pragma warning disable SA1118 // Parameter should not span multiple lines
|
||||
|
||||
namespace Microsoft.PowerFx.Repl.Tests
|
||||
{
|
||||
// Thesde tests should target public Module Import() function
|
||||
// (and not internal services)
|
||||
public class ModuleTests
|
||||
{
|
||||
private PowerFxREPL _repl;
|
||||
private readonly TestReplOutput _output = new TestReplOutput();
|
||||
|
||||
public ModuleTests()
|
||||
{
|
||||
var config = new PowerFxConfig();
|
||||
config.SymbolTable.EnableMutationFunctions();
|
||||
|
||||
// config.EnableSetFunction();
|
||||
var engine = new RecalcEngine(config);
|
||||
|
||||
_repl = new PowerFxREPL
|
||||
{
|
||||
Engine = engine,
|
||||
Output = _output,
|
||||
AllowSetDefinitions = true,
|
||||
AllowUserDefinedFunctions = true,
|
||||
AllowImport = true,
|
||||
ParserOptions = new ParserOptions() { AllowsSideEffects = true }
|
||||
};
|
||||
}
|
||||
|
||||
// Run line, return the normal output.
|
||||
private string HandleLine(string fx, bool expectErrors = false)
|
||||
{
|
||||
Debugger.Log(0, string.Empty, ">> " + fx);
|
||||
_repl.HandleLine(fx);
|
||||
|
||||
if (!expectErrors)
|
||||
{
|
||||
AssertNoErrors();
|
||||
}
|
||||
|
||||
var log = _output.Get(OutputKind.Repl);
|
||||
|
||||
Debugger.Log(0, string.Empty, log);
|
||||
return log;
|
||||
}
|
||||
|
||||
private void AssertNoErrors()
|
||||
{
|
||||
var errors = _output.Get(OutputKind.Error);
|
||||
Assert.Empty(errors);
|
||||
}
|
||||
|
||||
private string GetModuleFullPath(string path)
|
||||
{
|
||||
string fullpath = Path.Combine(
|
||||
Environment.CurrentDirectory,
|
||||
"Modules",
|
||||
path);
|
||||
|
||||
return fullpath;
|
||||
}
|
||||
|
||||
private void Import(string path, bool expectErrors = false)
|
||||
{
|
||||
string fullpath = GetModuleFullPath(path);
|
||||
|
||||
var expr = $"Import(\"{fullpath}\")";
|
||||
HandleLine(expr, expectErrors);
|
||||
|
||||
if (expectErrors)
|
||||
{
|
||||
var modules = _repl.Modules.ToArray();
|
||||
Assert.Empty(modules); // nothing actually loaded.
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that we only can access modules if they're enabled.
|
||||
[Fact]
|
||||
public void MustEnable()
|
||||
{
|
||||
var engine = new RecalcEngine();
|
||||
|
||||
_repl = new PowerFxREPL
|
||||
{
|
||||
Engine = engine,
|
||||
Output = _output
|
||||
};
|
||||
|
||||
// Defaults to false
|
||||
Assert.False(_repl.AllowUserDefinedFunctions);
|
||||
|
||||
Import("basic1.fx.yml", expectErrors: true);
|
||||
|
||||
// Failed to import.
|
||||
var msg = _output.Get(OutputKind.Error);
|
||||
Assert.Contains("'Import' is an unknown or unsupported function.", msg);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Test1()
|
||||
{
|
||||
Import("basic1.fx.yml");
|
||||
|
||||
// Call method defined in moduel.
|
||||
var log = HandleLine("DoubleIt(3)");
|
||||
|
||||
Assert.Equal("6", log);
|
||||
}
|
||||
|
||||
// Load multiple modules
|
||||
[Fact]
|
||||
public void Test2()
|
||||
{
|
||||
Import("basic1.fx.yml");
|
||||
Import("basic2.fx.yml");
|
||||
|
||||
// Call method defined in moduel.
|
||||
var log = HandleLine("DoubleIt(Add1(3))");
|
||||
|
||||
Assert.Equal("8", log);
|
||||
}
|
||||
|
||||
// Load with imports
|
||||
[Fact]
|
||||
public void TestImport()
|
||||
{
|
||||
Import("Depend2.fx.yml");
|
||||
|
||||
// Call method defined in moduel.
|
||||
var log = HandleLine("Func2(3)");
|
||||
|
||||
Assert.Equal("8", log);
|
||||
}
|
||||
|
||||
// Load with imports
|
||||
[Fact]
|
||||
public void TestImport2()
|
||||
{
|
||||
Import("Depend2.fx.yml");
|
||||
|
||||
// Call method defined in moduel.
|
||||
var log = HandleLine("Func2(3)");
|
||||
Assert.Equal("8", log);
|
||||
|
||||
// Inner dependencies are not exported
|
||||
log = HandleLine("Add1(3)", expectErrors: true);
|
||||
var msg = _output.Get(OutputKind.Error);
|
||||
Assert.Contains("Add1' is an unknown or unsupported function.", msg);
|
||||
|
||||
// But we can import the inner module and call.
|
||||
Import("basic2.fx.yml");
|
||||
log = HandleLine("Add1(3)");
|
||||
Assert.Equal("4", log);
|
||||
}
|
||||
|
||||
// Load with imports
|
||||
[Fact]
|
||||
public void Recursion()
|
||||
{
|
||||
Import("recursion.fx.yml");
|
||||
|
||||
// 0,1, 1, 2, 3, 5, 8,13
|
||||
var log = HandleLine("Fib(7)");
|
||||
Assert.Equal("13", log);
|
||||
}
|
||||
|
||||
// Load with imports
|
||||
[Fact]
|
||||
public void Shadow()
|
||||
{
|
||||
Import("shadow.fx.yml");
|
||||
|
||||
var log = HandleLine("Foo()");
|
||||
Assert.Equal("-2", log);
|
||||
|
||||
log = HandleLine("Abs(-7)"); // calls new one from module
|
||||
}
|
||||
|
||||
// Diamond inheritence.
|
||||
// 1 --> {2a, 2b}. 2a-->3. 2b-->3.
|
||||
// This is interesting since module 3 gets used multiple times, but it's not a cycle.
|
||||
[Fact]
|
||||
public void Diamond()
|
||||
{
|
||||
Import("diamond_1.fx.yml");
|
||||
|
||||
var log = HandleLine("Func1(5)");
|
||||
Assert.Equal("\"2A(3(5)),2B(3(5))\"", log);
|
||||
}
|
||||
|
||||
// Conflict if we import 2 modules that define the same symbols.
|
||||
[Fact]
|
||||
public void DuplicateSymbolsConflict()
|
||||
{
|
||||
Import("conflict1.fx.yml", expectErrors: true);
|
||||
var errorMsg = _output.Get(OutputKind.Error);
|
||||
|
||||
// message contains useful information.
|
||||
Assert.Contains("DoubleIt", errorMsg); // conflicting symbol
|
||||
Assert.Contains("basic1.fx.yml", errorMsg); // module 1
|
||||
Assert.Contains("basic1_dup.fx.yml", errorMsg); // module 2
|
||||
}
|
||||
|
||||
// Import() is a meta function and can only be called
|
||||
// at top-levle repl and not within a module itself.
|
||||
[Fact]
|
||||
public void ImportIsAMetafunction()
|
||||
{
|
||||
Import("ErrorImport.fx.yml", expectErrors: true);
|
||||
var errorMsg = _output.Get(OutputKind.Error);
|
||||
|
||||
Assert.Contains("'Import' is an unknown or unsupported function", errorMsg);
|
||||
}
|
||||
|
||||
// When we import a file twice, we get the updated contents.
|
||||
[Fact]
|
||||
public void GetsLatest()
|
||||
{
|
||||
using var temp = new TempFileHolder();
|
||||
string fullpath = temp.FullPath;
|
||||
|
||||
// First version
|
||||
File.WriteAllText(fullpath, @"
|
||||
Formulas: |
|
||||
Func(x: Number) : Number = x * 10;
|
||||
");
|
||||
HandleLine($"Import(\"{fullpath}\")");
|
||||
|
||||
// Call method defined in moduel.
|
||||
var before = HandleLine("Func(3)");
|
||||
Assert.Equal("30", before);
|
||||
|
||||
// Update the file, 2nd version
|
||||
File.WriteAllText(fullpath, @"
|
||||
Formulas: |
|
||||
Func(x: Number) : Number = x * 20;
|
||||
");
|
||||
|
||||
HandleLine($"Import(\"{fullpath}\")");
|
||||
|
||||
var after = HandleLine("Func(3)");
|
||||
Assert.Equal("60", after);
|
||||
}
|
||||
|
||||
// Error gets spans.
|
||||
[Fact]
|
||||
public void ErrorShowsFileRange()
|
||||
{
|
||||
Import("Error1.fx.yml", expectErrors: true);
|
||||
|
||||
var errorMsg = _output.Get(OutputKind.Error);
|
||||
|
||||
// Key elements here are that:
|
||||
// - the message has the filename
|
||||
// - the location (5,9) is relative into the file, not just the expression.
|
||||
Assert.Equal("Error: Error1.fx.yml (5,9): Name isn't valid. 'missing' isn't recognized.", errorMsg);
|
||||
}
|
||||
|
||||
// Circular references are detected
|
||||
[Fact]
|
||||
public void Cycles()
|
||||
{
|
||||
Import("cycle1.fx.yml", expectErrors: true);
|
||||
|
||||
var errorMsg = _output.Get(OutputKind.Error);
|
||||
Assert.Contains("Circular reference", errorMsg);
|
||||
}
|
||||
|
||||
// Loading a missing file
|
||||
[Fact]
|
||||
public void Missing()
|
||||
{
|
||||
Import("missing_file.fx.yml", expectErrors: true);
|
||||
|
||||
var errorMsg = _output.Get(OutputKind.Error);
|
||||
Assert.Contains("missing_file.fx.yml", errorMsg);
|
||||
}
|
||||
|
||||
// Loading a file with yaml parse errors.
|
||||
[Fact]
|
||||
public void YamlParseErrors()
|
||||
{
|
||||
Import("yaml_parse_errors.fx.yml", expectErrors: true);
|
||||
|
||||
var errorMsg = _output.Get(OutputKind.Error);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
Imports:
|
||||
- File: Basic1.fx.yml
|
||||
- File: Basic2.fx.yml
|
||||
|
||||
# Depends on Basic1
|
||||
Formulas: |
|
||||
Func2(x: Number) : Number = DoubleIt(Add1(x));
|
|
@ -0,0 +1,6 @@
|
|||
# Has an error in expression.
|
||||
# Test that error reporting gets right line.
|
||||
Formulas: |
|
||||
DoubleIt(x: Number) : Number =
|
||||
missing // <-- error is on line 5
|
||||
* 2;
|
|
@ -0,0 +1,7 @@
|
|||
# Error - we can't call Import() within a function.
|
||||
# must be a top-level yaml construct.
|
||||
|
||||
Formulas: |
|
||||
Func2(x: Number) : Void = {
|
||||
Import("Basic1.fx.yml")
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
# Most basic Power Fx module.
|
||||
Formulas: |
|
||||
DoubleIt(x: Number) : Number = x * 2;
|
|
@ -0,0 +1,4 @@
|
|||
# test symbol conflicts.
|
||||
# Same symbols as base1.fx.yml
|
||||
Formulas: |
|
||||
DoubleIt(x: Number) : Number = x * 200;
|
|
@ -0,0 +1,3 @@
|
|||
# Most basic Power Fx module.
|
||||
Formulas: |
|
||||
Add1(x: Number) : Number = x +1;
|
|
@ -0,0 +1,6 @@
|
|||
Imports:
|
||||
- File: basic1.fx.yml
|
||||
- File: basic1_dup.fx.yml
|
||||
|
||||
Formulas: |
|
||||
Run() : Number = DoubleIt(5);
|
|
@ -0,0 +1,6 @@
|
|||
# Test circular dependency
|
||||
Imports:
|
||||
- File: cycle2.fx.yml
|
||||
|
||||
Formulas: |
|
||||
Func1(x: Number) : Number = 1;
|
|
@ -0,0 +1,6 @@
|
|||
# Test circular dependency
|
||||
Imports:
|
||||
- File: cycle1.fx.yml
|
||||
|
||||
Formulas: |
|
||||
Func2(x: Number) : Number = 1;
|
|
@ -0,0 +1,7 @@
|
|||
Imports:
|
||||
- File: diamond_2a.fx.yml
|
||||
- File: diamond_2b.fx.yml
|
||||
|
||||
# Depends on 2a,2b. Both depend on 3.
|
||||
Formulas: |
|
||||
Func1(x: Text) : Text = Func2a(x) & "," & Func2b(x);
|
|
@ -0,0 +1,8 @@
|
|||
Imports:
|
||||
- File: diamond_3.fx.yml
|
||||
|
||||
|
||||
# Depends on Basic1
|
||||
Formulas: |
|
||||
Func2a(x: Text) : Text = $"2A({Func3(x)})";
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
Imports:
|
||||
- File: diamond_3.fx.yml
|
||||
|
||||
|
||||
# Depends on Basic1
|
||||
Formulas: |
|
||||
Func2b(x: Text) : Text = $"2B({Func3(x)})";
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
Formulas: |
|
||||
Func3(x: Text) : Text = $"3({x})";
|
|
@ -0,0 +1,4 @@
|
|||
# Define a basic recursive function.
|
||||
Formulas: |
|
||||
Fib(n: Number) : Number =
|
||||
If(n <= 1, n, Fib(n - 1) + Fib(n - 2));
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
Formulas: |
|
||||
// Redefine a builtin.
|
||||
// Give a different (wrong) implementation so we know which is called.
|
||||
Abs(x: Number) : Number = x *2;
|
||||
|
||||
// Binds to the one in this scope.
|
||||
Foo() : Number = Abs(-1);
|
|
@ -0,0 +1,3 @@
|
|||
# not a valid yaml file
|
||||
Formulas::
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CodeAnalysis.CodeFixes;
|
||||
using Microsoft.CodeAnalysis.VisualBasic.Syntax;
|
||||
using Microsoft.PowerFx.Types;
|
||||
using Xunit;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Microsoft.PowerFx.Repl.Tests
|
||||
{
|
||||
#pragma warning disable SA1117 // Parameters should be on same line or separate lines
|
||||
|
||||
public class StringWithSourceTests
|
||||
{
|
||||
private class Poco1
|
||||
{
|
||||
public StringWithSource Prop { get; set; }
|
||||
}
|
||||
|
||||
private class Poco1Direct
|
||||
{
|
||||
public string Prop { get; set; }
|
||||
}
|
||||
|
||||
// common fake filename used in locations.
|
||||
private const string Filename = "myfile1.txt";
|
||||
|
||||
private T Parse<T>(string contents)
|
||||
{
|
||||
// Deserialize.
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithTypeConverter(new StringWithSourceConverter(Filename, contents))
|
||||
.Build();
|
||||
|
||||
var poco = deserializer.Deserialize<T>(contents);
|
||||
|
||||
return poco;
|
||||
}
|
||||
|
||||
private T ParseExpectError<T>(string contents, string expectedError)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Deserialize.
|
||||
var deserializer = new DeserializerBuilder()
|
||||
.WithTypeConverter(new StringWithSourceConverter(Filename, contents))
|
||||
.Build();
|
||||
|
||||
var poco = deserializer.Deserialize<T>(contents);
|
||||
|
||||
Assert.Fail($"Expected error: {expectedError}");
|
||||
return poco;
|
||||
}
|
||||
catch (YamlException e)
|
||||
{
|
||||
// Exceptions from parser will be wrapped in YamlException.
|
||||
var msg = e?.InnerException?.Message ?? e.Message;
|
||||
Assert.Contains(expectedError, msg);
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
// In all cases, parse as 'string' should succeed.
|
||||
// But Parse with SourceLocaiton has more limitations.
|
||||
// expectedError specifies restrictions.
|
||||
private StringWithSource Test(string contents, string expectedError = null)
|
||||
{
|
||||
// Should always work with 'string'
|
||||
var p1 = Parse<Poco1Direct>(contents);
|
||||
|
||||
if (expectedError != null)
|
||||
{
|
||||
ParseExpectError<Poco1>(contents, expectedError);
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
var p2 = Parse<Poco1>(contents);
|
||||
|
||||
Assert.Equal(p1.Prop, p2.Prop.Value);
|
||||
Assert.Same(Filename, p2.Prop.Location.Filename);
|
||||
|
||||
if (p2.Prop.Value != null)
|
||||
{
|
||||
p2.Prop.Value = p2.Prop.Value.Replace("\r", string.Empty);
|
||||
}
|
||||
|
||||
return p2.Prop;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(
|
||||
@"Prop: |
|
||||
123
|
||||
", 2, 3, "123\n")]
|
||||
|
||||
// Leading space in front of property.
|
||||
[InlineData(
|
||||
@" Prop: |
|
||||
123
|
||||
", 2, 3, "123\n")]
|
||||
|
||||
[InlineData(
|
||||
@"# another line
|
||||
Prop: |
|
||||
123
|
||||
456", 3, 6, "123\n456")]
|
||||
|
||||
[InlineData(
|
||||
@"Prop: |-
|
||||
123
|
||||
", 2, 3, "123")]
|
||||
|
||||
[InlineData(
|
||||
@"Prop: |-
|
||||
true
|
||||
", 2, 3, "true")] // YDN will still return as string.
|
||||
|
||||
// Other encodings (not | ) are not supported.
|
||||
[InlineData(
|
||||
@"Prop: >
|
||||
123
|
||||
456
|
||||
", 0, 0, "123456\n", "literal")] // Folding
|
||||
|
||||
[InlineData(
|
||||
@"Prop: 123456
|
||||
", 0, 0, "123456", "literal")] // plain
|
||||
|
||||
[InlineData(
|
||||
@"Prop: ""123456""
|
||||
", 0, 0, "123456", "literal")] // quotes
|
||||
|
||||
[InlineData(
|
||||
@"Prop: ", 1, 6, null)] // empty
|
||||
|
||||
public void Test1(string contents, int lineStart, int colStart, string value, string errorMessage = null)
|
||||
{
|
||||
var p1 = Test(contents, errorMessage);
|
||||
|
||||
if (errorMessage == null)
|
||||
{
|
||||
Assert.Equal(value, p1.Value);
|
||||
Assert.Equal(colStart, p1.Location.ColStart);
|
||||
Assert.Equal(lineStart, p1.Location.LineStart);
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.Null(p1);
|
||||
}
|
||||
}
|
||||
|
||||
// Error conditions.
|
||||
[Theory]
|
||||
[InlineData(
|
||||
"Prop: { }")] // object , expecting string.
|
||||
public void TestError(string contents)
|
||||
{
|
||||
ParseExpectError<Poco1Direct>(contents, "Failed");
|
||||
ParseExpectError<Poco1>(contents, "Expected 'Scalar'");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// <copyright file="AssertFunction.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.PowerFx.Types;
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
|
||||
namespace Microsoft.PowerFx.Repl.Functions
|
||||
{
|
||||
/// <summary>
|
||||
/// Assert Function.
|
||||
/// </summary>
|
||||
internal class AssertFunction : ReflectionFunction
|
||||
{
|
||||
public AssertFunction()
|
||||
: base("Assert", FormulaType.Void, new[] { FormulaType.Boolean, FormulaType.String })
|
||||
{
|
||||
ConfigType = typeof(IReplOutput);
|
||||
}
|
||||
|
||||
public async Task<VoidValue> Execute(IReplOutput output, BooleanValue test, StringValue message, CancellationToken cancel)
|
||||
{
|
||||
if (test.Value)
|
||||
{
|
||||
await output.WriteLineAsync($"PASSED: {message.Value}", OutputKind.Notify, cancel)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await output.WriteLineAsync($"FAILED: {message.Value}", OutputKind.Error, cancel)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return FormulaValue.NewVoid();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -18,6 +18,8 @@ using Microsoft.PowerFx.Types;
|
|||
|
||||
namespace Microsoft.PowerFx
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
|
||||
public static class ConsoleRepl
|
||||
{
|
||||
private const string OptionFormatTable = "FormatTable";
|
||||
|
@ -84,9 +86,9 @@ namespace Microsoft.PowerFx
|
|||
|
||||
config.EnableSetFunction();
|
||||
config.EnableJsonFunctions();
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
config.EnableOptionSetInfo();
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
config.AddFunction(new AssertFunction());
|
||||
|
||||
config.AddFunction(new ResetFunction());
|
||||
config.AddFunction(new Option0Function());
|
||||
|
@ -97,9 +99,7 @@ namespace Microsoft.PowerFx
|
|||
|
||||
var optionsSet = new OptionSet("Options", DisplayNameUtility.MakeUnique(options));
|
||||
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
config.EnableRegExFunctions(new TimeSpan(0, 0, 5));
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
config.AddOptionSet(optionsSet);
|
||||
|
||||
|
@ -124,9 +124,7 @@ namespace Microsoft.PowerFx
|
|||
}
|
||||
|
||||
// Hook repl engine with customizations.
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
private class MyRepl : PowerFxREPL
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
public MyRepl()
|
||||
{
|
||||
|
@ -138,6 +136,8 @@ namespace Microsoft.PowerFx
|
|||
|
||||
this.AllowSetDefinitions = true;
|
||||
this.AllowUserDefinedFunctions = _enableUDFs;
|
||||
this.AllowImport = true;
|
||||
|
||||
this.EnableSampleUserObject();
|
||||
this.AddPseudoFunction(new IRPseudoFunction());
|
||||
this.AddPseudoFunction(new SuggestionsPseudoFunction());
|
||||
|
@ -427,9 +427,7 @@ namespace Microsoft.PowerFx
|
|||
|
||||
private class MyHelpProvider : HelpProvider
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
public override async Task Execute(PowerFxREPL repl, CancellationToken cancel, string context = null)
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
if (context?.ToLowerInvariant() == "options" || context?.ToLowerInvariant() == "option")
|
||||
{
|
||||
|
|
Загрузка…
Ссылка в новой задаче