├── AST.pq ├── AST2json.pq ├── Decompile.pq ├── Decompiler ├── Decompiler.sln └── Decompiler │ ├── Decompiler.mproj │ ├── Decompiler.pq │ ├── Decompiler.query.pq │ ├── Decompiler16.png │ ├── Decompiler20.png │ ├── Decompiler24.png │ ├── Decompiler32.png │ ├── Decompiler40.png │ ├── Decompiler48.png │ ├── Decompiler64.png │ ├── Decompiler80.png │ ├── Diagnostics.pq │ ├── RowExpression.From.pq │ └── resources.resx ├── Expression.pq ├── FunctionSignature.pq ├── LICENSE ├── README.md └── img └── DecompileCall.PNG /AST.pq: -------------------------------------------------------------------------------- 1 | =Value.ResourceExpression(List.Range)[Expression][Arguments]{0}[Function][Expression][Expression] -------------------------------------------------------------------------------- /AST2json.pq: -------------------------------------------------------------------------------- 1 | (placeholder) => 2 | let 3 | resource = Value.ResourceExpression(placeholder), 4 | func = (_) => try Value.Metadata(Value.Type(_))[Documentation.Name] otherwise 5 | try Value.ResourceExpression(_)[Name] otherwise "function", 6 | trace = (_) => if _ is function then func(_) 7 | else if _ is type then "type" 8 | else if _ is null then "null" 9 | else if _ is list then List.Accumulate(_,{},(s,c)=>s&{@trace(c)}) 10 | else if _ is record then List.Accumulate(Record.FieldNames(_),[],(s,c)=>Record.AddField(s,c,@trace(Record.Field(_,c)))) 11 | else Text.From(_) 12 | in Json.FromValue(trace(resource)) -------------------------------------------------------------------------------- /Decompile.pq: -------------------------------------------------------------------------------- 1 | (fn) => 2 | let 3 | // Entry point for routing the calls for various AST node "Kind"s 4 | handleExp = (x) => 5 | let 6 | kind = x[Kind], 7 | expr = if (kind = "Invocation") then invocationExp(x) else 8 | if (kind = "ElementAccess") then elementAccessExp(x) else 9 | if (kind = "If") then ifExp(x) else 10 | if (kind = "Binary") then binaryExp(x) else 11 | if (kind = "FieldAccess") then fieldAccessExp(x) else 12 | if (kind = "Identifier") then identifierExp(x) else 13 | if (kind = "Constant") then constantExp(x) else 14 | Text.Format("Unkonw Kind: #{0}",{kind}) 15 | in 16 | expr, 17 | 18 | // "Invocation" node handling 19 | invocationExp = (x) => 20 | let 21 | f = x[Function], 22 | args = x[Arguments], 23 | funcStr = handleExp(f), 24 | argsStr = Text.Combine(List.Transform(args, each handleExp(_)),",") 25 | in 26 | Text.Format("#{0}(#{1})",{funcStr, argsStr}), 27 | 28 | // Get function name from a handle 29 | // TODO: have a peek at #shared first as docs are missleading 30 | functionName = (x) => 31 | //try getting the name from the associated meta Documentation 32 | try Value.Metadata(Value.Type(x))[Documentation.Name] 33 | otherwise 34 | // or "ResourceExpression" 35 | try Value.ResourceExpression(x)[Name] otherwise "function", 36 | 37 | // "If" node handling 38 | ifExp = (x) => 39 | let 40 | cond = handleExp(x[Condition]), 41 | left = handleExp(x[TrueCase]), 42 | right = handleExp(x[FalseCase]) 43 | in 44 | Text.Format("if(#{0}) then #{1} else #{2}", { cond, left, right }), 45 | 46 | // "Operator" node handling 47 | operatorExp = (x) => 48 | let 49 | op = 50 | if (x = "Equals") then "=" else 51 | if (x = "NotEquals") then "!=" else 52 | if (x = "GreaterThan") then ">" else 53 | if (x = "GreaterThanOrEquals") then ">=" else 54 | if (x = "LessThan") then "<" else 55 | if (x = "LessThanOrEquals") then "<=" else 56 | if (x = "And") then "and" else 57 | if (x = "Or") then "or" else 58 | if (x = "Not") then "not" else 59 | if (x = "Add") then "+" else 60 | if (x = "Subtract") then "-" else 61 | if (x = "Multiply") then "*" else 62 | if (x = "Divide") then "/" else 63 | if (x = "Concatenate") then "&" else 64 | x 65 | in 66 | op, 67 | // "Identifier" node handling 68 | identifierExp = (x) => x[Name], 69 | // "Constnt" node handling 70 | constantExp = (x) => x[Value], 71 | // "FieldAccess" node handling 72 | fieldAccessExp = (x) => x[MemberName], 73 | 74 | // "ElementAccess" node handling 75 | elementAccessExp = (x) => 76 | let 77 | refList = x[Collection][Value], 78 | idx = x[Key][Value] 79 | in functionName(refList{idx}), 80 | 81 | // Handles node of a "Binary" expression 82 | binaryExp = (x) => 83 | let 84 | op = operatorExp(x[Operator]), 85 | left = handleExp(x[Left]), 86 | right = handleExp(x[Right]), 87 | format = if (op = "&") then "#{0} & #{2}" 88 | else "#{0} #{1} #{2}" 89 | in 90 | Text.Format(format, {left, op, right}), 91 | 92 | // Looping through the Members of a shallow AST and calls expression->text function:handleExp(x) 93 | run = (x)=> 94 | let 95 | members = x[Members] 96 | in 97 | List.Transform(members,each Text.Format("#{0} = #{1}",{_[Name],handleExp(_[Value])})), 98 | 99 | AST = Value.ResourceExpression(fn)[Expression][Arguments]{0}[Function][Expression][Expression], 100 | expression = run(AST) 101 | in 102 | expression -------------------------------------------------------------------------------- /Decompiler/Decompiler.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2010 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{4DF76451-A46A-4C0B-BE03-459FAAFA07E6}") = "Decompiler", "Decompiler\Decompiler.mproj", "{E6D19E2E-3BB6-466B-A579-C4F840F09D4B}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x86 = Debug|x86 11 | Release|x86 = Release|x86 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {E6D19E2E-3BB6-466B-A579-C4F840F09D4B}.Debug|x86.ActiveCfg = Debug|x86 15 | {E6D19E2E-3BB6-466B-A579-C4F840F09D4B}.Debug|x86.Build.0 = Debug|x86 16 | {E6D19E2E-3BB6-466B-A579-C4F840F09D4B}.Release|x86.ActiveCfg = Release|x86 17 | {E6D19E2E-3BB6-466B-A579-C4F840F09D4B}.Release|x86.Build.0 = Release|x86 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {86AFB98A-7D7C-4F27-953B-B4136DECB826} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler.mproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Debug 4 | 2.0 5 | 6 | 7 | Exe 8 | MyRootNamespace 9 | MyAssemblyName 10 | False 11 | False 12 | False 13 | False 14 | False 15 | False 16 | False 17 | False 18 | False 19 | False 20 | 1000 21 | Yes 22 | Decompiler 23 | 24 | 25 | false 26 | 27 | bin\Debug\ 28 | 29 | 30 | false 31 | bin\Release\ 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | Code 42 | 43 | 44 | Code 45 | 46 | 47 | Code 48 | 49 | 50 | Code 51 | 52 | 53 | Code 54 | 55 | 56 | Code 57 | 58 | 59 | Code 60 | 61 | 62 | Code 63 | 64 | 65 | Code 66 | 67 | 68 | Code 69 | 70 | 71 | Code 72 | 73 | 74 | Content 75 | 76 | 77 | Content 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler.pq: -------------------------------------------------------------------------------- 1 | // This file contains your Data Connector logic 2 | section Decompiler; 3 | 4 | [DataSource.Kind="Decompiler", Publish="Decompiler.Publish"] 5 | shared Decompiler.Contents = (optional message as text) => 6 | let 7 | _message = if (message <> null) then message else "(no message)", 8 | a = "Hello from Decompiler: " & _message 9 | in 10 | a; 11 | 12 | // Data Source Kind description 13 | Decompiler = [ 14 | Authentication = [ 15 | // Key = [], 16 | // UsernamePassword = [], 17 | // Windows = [], 18 | Implicit = [] 19 | ], 20 | Label = Extension.LoadString("DataSourceLabel") 21 | ]; 22 | 23 | // Data Source UI publishing description 24 | Decompiler.Publish = [ 25 | Beta = true, 26 | Category = "Other", 27 | ButtonText = { Extension.LoadString("ButtonTitle"), Extension.LoadString("ButtonHelp") }, 28 | LearnMoreUrl = "https://powerbi.microsoft.com/", 29 | SourceImage = Decompiler.Icons, 30 | SourceTypeImage = Decompiler.Icons 31 | ]; 32 | 33 | Decompiler.Icons = [ 34 | Icon16 = { Extension.Contents("Decompiler16.png"), Extension.Contents("Decompiler20.png"), Extension.Contents("Decompiler24.png"), Extension.Contents("Decompiler32.png") }, 35 | Icon32 = { Extension.Contents("Decompiler32.png"), Extension.Contents("Decompiler40.png"), Extension.Contents("Decompiler48.png"), Extension.Contents("Decompiler64.png") } 36 | ]; 37 | 38 | 39 | // Utils 40 | 41 | 42 | Extension.LoadFunction = (name as text) => 43 | let 44 | binary = Extension.Contents(name), 45 | asText = Text.FromBinary(binary) 46 | in 47 | Expression.Evaluate(asText, #shared); 48 | 49 | RowExpression.From = Extension.LoadFunction("RowExpression.From.pq"); 50 | 51 | Diagnostics = Extension.LoadFunction("Diagnostics.pq"); 52 | 53 | Diagnostics.LogValue = Diagnostics[LogValue]; 54 | Diagnostics.LogValue2 = Diagnostics[LogValue2]; 55 | Diagnostics.LogFailure = Diagnostics[LogFailure]; 56 | Diagnostics.WrapFunctionResult = Diagnostics[WrapFunctionResult]; 57 | Diagnostics.WrapHandlers = Diagnostics[WrapHandlers]; 58 | 59 | Value.IfNull = (a, b) => if a <> null then a else b; 60 | 61 | Value.ToText = (value, optional depth) => 62 | let 63 | nextDepth = if depth = null then 3 else depth - 1, 64 | result = if depth = 0 then "..." 65 | else if value is null then "" 66 | else if value is function then "" 67 | else if value is table then "#table({" & Text.Combine(Table.ColumnNames(value), ",") & "},{" & Text.Combine( 68 | List.Transform(Table.ToRows(Table.FirstN(value, 2)), each @Value.ToText(_, nextDepth)), "},#(cr)#(lf){") & "})" 69 | //& "Row Count (" & Number.ToText(Table.RowCount(value)) & ")" 70 | else if value is list then "{" & Text.Combine(List.Transform(List.FirstN(value, 10), each @Value.ToText(_, nextDepth)), ",") & "}" 71 | else if value is record then "[" & Text.Combine(List.Transform(Record.FieldNames(value), each _ & "=" & @Value.ToText(Record.Field(value, _), nextDepth)), ",") & "]" 72 | else if value is type then List.First(Table.Schema(#table({"type"}, {{value}}))[TypeName], "") 73 | else Text.From(value) 74 | in 75 | try result otherwise ""; -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler.query.pq: -------------------------------------------------------------------------------- 1 | // Use this file to write queries to test your data connector 2 | let 3 | result = Decompiler.Contents() 4 | in 5 | result -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryDecompiler/d1bf7ba7fc6a9aec0325d45fbc0570b8f45bbe7c/Decompiler/Decompiler/Decompiler16.png -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryDecompiler/d1bf7ba7fc6a9aec0325d45fbc0570b8f45bbe7c/Decompiler/Decompiler/Decompiler20.png -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryDecompiler/d1bf7ba7fc6a9aec0325d45fbc0570b8f45bbe7c/Decompiler/Decompiler/Decompiler24.png -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryDecompiler/d1bf7ba7fc6a9aec0325d45fbc0570b8f45bbe7c/Decompiler/Decompiler/Decompiler32.png -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryDecompiler/d1bf7ba7fc6a9aec0325d45fbc0570b8f45bbe7c/Decompiler/Decompiler/Decompiler40.png -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryDecompiler/d1bf7ba7fc6a9aec0325d45fbc0570b8f45bbe7c/Decompiler/Decompiler/Decompiler48.png -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryDecompiler/d1bf7ba7fc6a9aec0325d45fbc0570b8f45bbe7c/Decompiler/Decompiler/Decompiler64.png -------------------------------------------------------------------------------- /Decompiler/Decompiler/Decompiler80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryDecompiler/d1bf7ba7fc6a9aec0325d45fbc0570b8f45bbe7c/Decompiler/Decompiler/Decompiler80.png -------------------------------------------------------------------------------- /Decompiler/Decompiler/Diagnostics.pq: -------------------------------------------------------------------------------- 1 | let 2 | Value.ToText = (value, optional depth) => 3 | let 4 | nextDepth = if depth = null then 3 else depth - 1, 5 | result = if depth = 0 then "..." 6 | else if value is null then "" 7 | else if value is function then "" 8 | else if value is table then "#table({" & Text.Combine(Table.ColumnNames(value), ",") & "},{" & Text.Combine( 9 | List.Transform(Table.ToRows(Table.FirstN(value, 2)), each @Value.ToText(_, nextDepth)), "},#(cr)#(lf){") & "})" 10 | //& "Row Count (" & Number.ToText(Table.RowCount(value)) & ")" 11 | else if value is list then "{" & Text.Combine(List.Transform(List.FirstN(value, 10), each @Value.ToText(_, nextDepth)), ",") & "}" 12 | else if value is record then "[" & Text.Combine(List.Transform(Record.FieldNames(value), each _ & "=" & @Value.ToText(Record.Field(value, _), nextDepth)), ",") & "]" 13 | else if value is type then List.First(Table.Schema(#table({"type"}, {{value}}))[TypeName], "") 14 | else Text.From(value) 15 | in 16 | try result otherwise "", 17 | Diagnostics.LogValue = (prefix, value, result, optional delayed) => Diagnostics.Trace(TraceLevel.Information, prefix & ": " & Value.ToText(value), result, delayed), 18 | Diagnostics.LogValue2 = (prefix, value) => Diagnostics.Trace(TraceLevel.Information, prefix & ": " & Value.ToText(value), value), 19 | Diagnostics.LogFailure = (text, function) => 20 | let 21 | result = try function() 22 | in 23 | if result[HasError] then Diagnostics.LogValue(text, result[Error], () => error result[Error], true) else result[Value], 24 | Diagnostics.WrapFunctionResult = (innerFunction as function, outerFunction as function) as function => 25 | Function.From(Value.Type(innerFunction), (list) => outerFunction(Function.Invoke(innerFunction, list))), 26 | Diagnostics.WrapHandlers = (handlers as record) as record => 27 | Record.FromList( 28 | List.Transform( 29 | Record.FieldNames(handlers), 30 | (h) => Diagnostics.WrapFunctionResult(Record.Field(handlers, h), (fn) => Diagnostics.LogFailure(h, fn))), 31 | Record.FieldNames(handlers)) 32 | in 33 | [ LogValue = Diagnostics.LogValue, LogValue2 = Diagnostics.LogValue2, LogFailure = Diagnostics.LogFailure, WrapFunctionResult = Diagnostics.WrapFunctionResult, WrapHandlers = Diagnostics.WrapHandlers] -------------------------------------------------------------------------------- /Decompiler/Decompiler/RowExpression.From.pq: -------------------------------------------------------------------------------- 1 | (fn, depth)=> 2 | let 3 | Value.FixType = (value, optional depth) => 4 | let 5 | nextDepth = if depth = null then 3 else depth - 1, 6 | result = if depth = 0 then null 7 | else if value is type then TextType(value) 8 | else if value is table then Table.TransformColumns(value, {}, @Value.FixType) 9 | else if value is list then List.Transform(value, each @Value.FixType(_, nextDepth)) 10 | else if value is record then 11 | Record.FromList(List.Transform(Record.ToList(value), each @Value.FixType(_, nextDepth)), Record.FieldNames(value)) 12 | else if value is function then "" 13 | else value 14 | in 15 | result, 16 | 17 | TextType = (t as type) as text => 18 | let 19 | nonNullableType = Type.NonNullable(t), 20 | TypeDescription = if Type.Is(nonNullableType, type binary) then "binary" 21 | else if Type.Is(nonNullableType, type date) then "date" 22 | else if Type.Is(nonNullableType, type datetime) then "datetime" 23 | else if Type.Is(nonNullableType, type datetimezone) then "datetimezone" 24 | else if Type.Is(nonNullableType, type duration) then "duration" 25 | else if Type.Is(nonNullableType, type function) then "function" 26 | else if Type.Is(nonNullableType, type list) then "list" 27 | else if Type.Is(nonNullableType, type logical) then "logical" 28 | else if Type.Is(nonNullableType, type none) then "none" 29 | else if Type.Is(nonNullableType, type null) then "null" 30 | else if Type.Is(nonNullableType, type number) then "number" 31 | else if Type.Is(nonNullableType, type record) then "record" 32 | else if Type.Is(nonNullableType, type table) then "table" 33 | else if Type.Is(nonNullableType, type text) then "text" 34 | else if Type.Is(nonNullableType, type time) then "time" 35 | else if Type.Is(nonNullableType, type type) then "type" 36 | else if Type.Is(nonNullableType, type action) then "action" 37 | else if Type.Is(type anynonnull, nonNullableType) then "any" 38 | else error "Unknown type", 39 | TypeString = if TypeDescription = "any" then 40 | if Type.IsNullable(t) then 41 | "any" else "anynonnull" 42 | else if Type.IsNullable(t) then 43 | "nullable " & TypeDescription 44 | else TypeDescription 45 | in 46 | TypeString, 47 | 48 | FunctionSignature = (functionType as type) => 49 | let 50 | Parameters = Record.ToTable(Type.FunctionParameters(functionType)), // Name, Value 51 | WithIndex = Table.AddIndexColumn(Parameters, "Required"), 52 | MinParameters = Type.FunctionRequiredParameters(functionType), 53 | WithRequired = Table.TransformColumns(WithIndex, {{"Required", (index) => index < MinParameters}}), 54 | WithNullable = Table.AddColumn(WithRequired, "Nullable", each Type.IsNullable([Value])), 55 | WithMetadata = Table.AddColumn(WithNullable, "Metadata", each Value.Metadata([Value])), 56 | WithDocumentation = Table.ExpandRecordColumn(WithMetadata, "Metadata", 57 | {"Documentation.FieldCaption", "Documentation.FieldDescription", "Documentation.SampleValues", "Documentation.AllowedValues", "Documentation.DefaultValue"}, 58 | {"fieldCaption", "fieldDescription", "sampleValues", "allowedValue", "defaultValue"}), 59 | // TODO: Add documentation to the allowed values 60 | Renamed = Table.RenameColumns(WithDocumentation, 61 | { 62 | {"Name", "name"}, 63 | {"Value", "parameterType"}, 64 | {"Required", "isRequired"}, 65 | {"Nullable", "isNullable"} 66 | }) 67 | in 68 | Renamed, 69 | 70 | Value.ToJsonText = (x,d) => Text.FromBinary(Json.FromValue(Value.FixType(x, d))), 71 | Result = Value.ToJsonText(fn,depth) 72 | in 73 | Result -------------------------------------------------------------------------------- /Decompiler/Decompiler/resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Connect to Decompiler 122 | 123 | 124 | Decompiler 125 | 126 | 127 | Decompiler 128 | 129 | -------------------------------------------------------------------------------- /Expression.pq: -------------------------------------------------------------------------------- 1 | let 2 | // Entry point for routing the calls for various AST node "Kind"s 3 | // TODO: throw 4 | handleExp = (x) => 5 | let 6 | kind = x[Kind], 7 | expr = if (kind = "Invocation") then invocationExp(x) else 8 | if (kind = "ElementAccess") then elementAccessExp(x) else 9 | if (kind = "If") then ifExp(x) else 10 | if (kind = "Binary") then binaryExp(x) else 11 | if (kind = "FieldAccess") then fieldAccessExp(x) else 12 | if (kind = "Identifier") then identifierExp(x) else 13 | if (kind = "Constant") then constantExp(x) else 14 | if (kind = "Record") then recordExp(x) else 15 | if (kind = "Unary") then unaryExp(x) else 16 | if (kind = "Throw") then throwExp(x) else 17 | if (kind = "Function") then funcExp(x) else 18 | Text.Format("Unkonw Kind: #{0}",{kind}) 19 | in 20 | expr, 21 | 22 | // Handle records 23 | recordExp = (x) => 24 | let 25 | members = x[Members], 26 | lines = List.Transform(members, 27 | each Text.Format("#{0} = #{1}", 28 | {_[Name],handleExp(_[Value])}) 29 | ) 30 | in 31 | Text.Combine(lines,"#(cr)#(lf)"), 32 | 33 | // Handle Unary expressions 34 | unaryExp = (x) => 35 | let 36 | op = x[Operator], 37 | inExp = x[Expression], 38 | kind = inExp[Kind], 39 | exp = if (op = "Not") then "not (" & handleExp(inExp) & ")" else 40 | if (op = "Negative") then "-(" & handleExp(inExp) & ")" else 41 | handleExp(inExp) 42 | in 43 | exp, 44 | 45 | // Throw 46 | throwExp = (x) =>Text.Format("error #{0}", handleExp(x[Expression])), 47 | 48 | // Function kind 49 | funcExp = (x) =>"[function]", 50 | 51 | // "Invocation" node handling 52 | invocationExp = (x) => 53 | let 54 | f = x[Function], 55 | args = x[Arguments], 56 | funcStr = handleExp(f), 57 | argsStr = Text.Combine(List.Transform(args, each handleExp(_)),",") 58 | in 59 | Text.Format("#{0}(#{1})",{funcStr, argsStr}), 60 | 61 | // Get function name from a handle 62 | // TODO: have a peek at #shared first as docs are missleading 63 | functionName = (x) => 64 | //try getting the name from the associated meta Documentation 65 | try Value.Metadata(Value.Type(x))[Documentation.Name] 66 | otherwise 67 | // or "ResourceExpression" 68 | try Value.ResourceExpression(x)[Name] otherwise "function", 69 | 70 | // "If" node handling 71 | ifExp = (x) => 72 | let 73 | cond = handleExp(x[Condition]), 74 | left = handleExp(x[TrueCase]), 75 | right = handleExp(x[FalseCase]) 76 | in 77 | Text.Format("if(#{0}) then #{1} else #{2}", { cond, left, right }), 78 | 79 | // "Operator" node handling 80 | operatorExp = (x) => 81 | let 82 | op = 83 | if (x = "Equals") then "=" else 84 | if (x = "NotEquals") then "!=" else 85 | if (x = "GreaterThan") then ">" else 86 | if (x = "GreaterThanOrEquals") then ">=" else 87 | if (x = "LessThan") then "<" else 88 | if (x = "LessThanOrEquals") then "<=" else 89 | if (x = "And") then "and" else 90 | if (x = "Or") then "or" else 91 | if (x = "Not") then "not" else 92 | if (x = "Add") then "+" else 93 | if (x = "Subtract") then "-" else 94 | if (x = "Multiply") then "*" else 95 | if (x = "Divide") then "/" else 96 | if (x = "Concatenate") then "&" else 97 | x 98 | in 99 | op, 100 | // "Identifier" node handling 101 | identifierExp = (x) => x[Name], 102 | // "Constnt" node handling 103 | constantExp = (x) => x[Value], 104 | // "FieldAccess" node handling 105 | fieldAccessExp = (x) => x[MemberName], 106 | 107 | // "ElementAccess" node handling 108 | elementAccessExp = (x) => 109 | let 110 | refList = x[Collection][Value], 111 | idx = x[Key][Value] 112 | in functionName(refList{idx}), 113 | 114 | // Handles node of a "Binary" expression 115 | binaryExp = (x) => 116 | let 117 | op = operatorExp(x[Operator]), 118 | left = handleExp(x[Left]), 119 | right = handleExp(x[Right]), 120 | format = if (op = "&") then "#{0} & #{2}" 121 | else "#{0} #{1} #{2}" 122 | in 123 | Text.Format(format, {left, op, right}), 124 | 125 | // Looping through the Members of a shallow AST and calls expression->text function:handleExp(x) 126 | run = (x)=> 127 | let 128 | members = x[Members] 129 | in 130 | List.Transform(members, 131 | each Text.Format("#{0} = #{1}", 132 | {_[Name],handleExp(_[Value])}) 133 | ), 134 | 135 | // Function to decompile 136 | fn = List.Range, 137 | // Getting a "shalow" view on the AST 138 | // TODO: replace with a search 139 | AST = 140 | try Value.ResourceExpression(fn)[Expression][Function][Expression] otherwise 141 | Value.ResourceExpression(fn)[Expression][Arguments]{0}[Function][Expression][Expression], 142 | // Start 143 | expression = run(AST) 144 | in 145 | expression -------------------------------------------------------------------------------- /FunctionSignature.pq: -------------------------------------------------------------------------------- 1 | (placeholder as function)=> 2 | let 3 | //Serialize type to text 4 | TypeAsText = (value as any) => 5 | let 6 | prefix = if Type.IsNullable(value) then "nullable " else "" 7 | in 8 | prefix&( 9 | if Type.Is(value, type binary) then "binary" else 10 | if Type.Is(value, type date) then "date" else 11 | if Type.Is(value, type datetime) then "datetime" else 12 | if Type.Is(value, type datetimezone) then "datetimezone" else 13 | if Type.Is(value, type duration) then "duration" else 14 | if Type.Is(value, type function) then "function" else 15 | if Type.Is(value, type list) then "list" else 16 | if Type.Is(value, type logical) then "logical" else 17 | if Type.Is(value, type none) then "none" else 18 | if Type.Is(value, type null) then "null" else 19 | if Type.Is(value, type number) then "number" else 20 | if Type.Is(value, type record) then "record" else 21 | if Type.Is(value, type table) then "table" else 22 | if Type.Is(value, type text) then "text" else 23 | if Type.Is(value, type time) then "time" else 24 | if Type.Is(value, type type) then "type" else 25 | if Type.Is(value, type any) then "any" 26 | else error "unknown"), 27 | //if parameter is Optional set prefix 28 | OptionalPrefix = (_)=>if Type.IsNullable(_) then "optional " else "", 29 | //get list of function parameters 30 | parameters = Type.FunctionParameters(Value.Type(placeholder)), 31 | //create a text list of parameters and associate types "[optional] paramname as type" 32 | parametersWithTypes = List.Accumulate(Record.FieldNames(parameters),{}, 33 | (state,cur)=>state&{ 34 | OptionalPrefix(Record.Field(parameters,cur))& 35 | cur&" as "&TypeAsText(Record.Field(parameters,cur))}) 36 | in 37 | //merge parameter list and prefix with "function (" and suffix with function return type 38 | "function ("& 39 | Text.Combine(parametersWithTypes,", ")& 40 | ") as "& 41 | TypeAsText(Type.FunctionReturn(Value.Type(placeholder))) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Power Query Decompiler 2 | This repository contains a proof of concept on decompiling Power Query (M) functions. 3 | ![Output](img/DecompileCall.PNG) 4 | 5 | ## Structure 6 | 7 | `AST.pq` - a deep call to `Value.ResourceExpression` in order to demo how the Abstract Syntax Tree (AST) looks for `List.Range` 8 | ``` 9 | =Value.ResourceExpression(List.Range) 10 | [Expression] 11 | [Arguments]{0} 12 | [Function] 13 | [Expression] 14 | [Expression] 15 | ``` 16 | 17 | `AST2json.pq` - serializing the output from `Value.ResourceExpression` to `JSON` 18 | 19 | `Decompile.pq` - function definition that translates an AST into an expression 20 | 21 | `Expression.pq` - same code as `Decompile(x)` function exposed as an expression(query) 22 | 23 | ## Functions that are "decompilable" 24 | 25 | The query that would return the list of functions that should deompile is: 26 | ``` 27 | = List.Select(Record.FieldNames(#shared),each Value.ResourceExpression(Record.Field(#shared,_))[Kind]="Function") 28 | ``` 29 | Which brings up this output: 30 | 31 | `Date.DayOfWeekName` , `Date.IsInCurrentDay` , `Date.IsInCurrentMonth` , `Date.IsInCurrentQuarter` , `Date.IsInCurrentWeek` , `Date.IsInCurrentYear` , `Date.IsInNextDay` , `Date.IsInNextMonth` , `Date.IsInNextNDays` , `Date.IsInNextNMonths` , `Date.IsInNextNQuarters` , `Date.IsInNextNWeeks` , `Date.IsInNextNYears` , `Date.IsInNextQuarter` , `Date.IsInNextWeek` , `Date.IsInNextYear` , `Date.IsInPreviousDay` , `Date.IsInPreviousMonth` , `Date.IsInPreviousNDays` , `Date.IsInPreviousNMonths` , `Date.IsInPreviousNQuarters` , `Date.IsInPreviousNWeeks` , `Date.IsInPreviousNYears` , `Date.IsInPreviousQuarter` , `Date.IsInPreviousWeek` , `Date.IsInPreviousYear` , `Date.IsInYearToDate` , `Date.MonthName` , `DateTime.IsInCurrentHour` , `DateTime.IsInCurrentMinute` , `DateTime.IsInCurrentSecond` , `DateTime.IsInNextHour` , `DateTime.IsInNextMinute` , `DateTime.IsInNextNHours` , `DateTime.IsInNextNMinutes` , `DateTime.IsInNextNSeconds` , `DateTime.IsInNextSecond` , `DateTime.IsInPreviousHour` , `DateTime.IsInPreviousMinute` , `DateTime.IsInPreviousNHours` , `DateTime.IsInPreviousNMinutes` , `DateTime.IsInPreviousNSeconds` , `DateTime.IsInPreviousSecond` , `List.FindText` , `List.MatchesAll` , `List.MatchesAny` , `List.NonNullCount` , `List.Range` , `List.RemoveItems` , `List.RemoveLastN` , `List.ReplaceValue` , `Replacer.ReplaceValue` , `SqlExpression.SchemaFrom` , `Table.AddColumn` , `Table.AlternateRows` , `Table.Buffer` , `Table.ColumnCount` , `Table.ColumnsOfType` , `Table.CombineColumns` , `Table.Contains` , `Table.ContainsAll` , `Table.ContainsAny` , `Table.DemoteHeaders` , `Table.DuplicateColumn` , `Table.ExpandListColumn` , `Table.ExpandTableColumn` , `Table.FillUp` , `Table.FindText` , `Table.FirstValue` , `Table.HasColumns` , `Table.InsertRows` , `Table.IsDistinct` , `Table.IsEmpty` , `Table.Last` , `Table.LastN` , `Table.MatchesAllRows` , `Table.MatchesAnyRows` , `Table.Max` , `Table.MaxN` , `Table.Min` , `Table.MinN` , `Table.Partition` , `Table.PositionOf` , `Table.PositionOfAny` , `Table.PrefixColumns` , `Table.Profile` , `Table.Range` , `Table.RemoveLastN` , `Table.RemoveMatchingRows` , `Table.RemoveRows` , `Table.Repeat` , `Table.ReplaceMatchingRows` , `Table.ReplaceRows` , `Table.ReplaceValue` , `Table.ReverseRows` , `Table.Schema` , `Table.SplitColumn` , `Table.ToColumns` , `Table.ToRows` , `Table.TransformRows` , `Table.Transpose` , `Table.View` , `Text.AfterDelimiter` , `Text.BeforeDelimiter` , `Text.BetweenDelimiters` , `Text.Format` , `Type.TableSchema` 32 | -------------------------------------------------------------------------------- /img/DecompileCall.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hugoberry/PowerQueryDecompiler/d1bf7ba7fc6a9aec0325d45fbc0570b8f45bbe7c/img/DecompileCall.PNG --------------------------------------------------------------------------------