Skip to content

Commit 09701de

Browse files
committed
Added binding attribute validator (#8256) and binding syntax (#8257) analyzers.
1 parent bfecdc2 commit 09701de

14 files changed

+1263
-5
lines changed
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Reflection;
9+
using Microsoft.Azure.WebJobs;
10+
using Microsoft.Azure.WebJobs.Host;
11+
using Microsoft.Azure.WebJobs.Hosting;
12+
using Microsoft.CodeAnalysis;
13+
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.Extensions.Hosting;
15+
16+
namespace Microsoft.Azure.Functions.Analyzers
17+
{
18+
// Only support extensions and WebJobs core.
19+
// Although extensions may refer to other dlls.
20+
public class AssemblyCache
21+
{
22+
// Map from assembly identities to full paths
23+
public static AssemblyCache Instance = new AssemblyCache();
24+
25+
bool _registered;
26+
27+
// Assembly Display Name --> Path
28+
Dictionary<string, string> _assemblyNameToPathMap = new Dictionary<string, string>();
29+
30+
// Assembly Display Name --> loaded Assembly object
31+
Dictionary<string, Assembly> _assemblyNameToObjMap = new Dictionary<string, Assembly>();
32+
33+
const string WebJobsAssemblyName = "Microsoft.Azure.WebJobs";
34+
const string WebJobsHostAssemblyName = "Microsoft.Azure.WebJobs.Host";
35+
36+
JobHostMetadataProvider _tooling;
37+
38+
internal JobHostMetadataProvider Tooling => _tooling;
39+
private int _projectCount;
40+
41+
// $$$ This can get invoked multiple times concurrently
42+
// This will get called on every compilation.
43+
// So return early on subsequent initializations.
44+
internal void Build(Compilation compilation)
45+
{
46+
Register();
47+
48+
int count;
49+
lock (this)
50+
{
51+
// If project references have changed, then reanalyze to pick up new dependencies.
52+
var refs = compilation.References.OfType<PortableExecutableReference>().ToArray();
53+
count = refs.Length;
54+
if ((count == _projectCount) && (_tooling != null))
55+
{
56+
return; // already initialized.
57+
}
58+
59+
// Even for netStandard/.core projects, this will still be a flattened list of the full transitive closure of dependencies.
60+
foreach (var asm in compilation.References.OfType<PortableExecutableReference>())
61+
{
62+
var dispName = asm.Display; // For .net core, the displayname can be the full path
63+
var path = asm.FilePath;
64+
65+
_assemblyNameToPathMap[dispName] = path;
66+
}
67+
68+
// Builtins
69+
_assemblyNameToObjMap["mscorlib"] = typeof(object).Assembly;
70+
_assemblyNameToObjMap[WebJobsAssemblyName] = typeof(Microsoft.Azure.WebJobs.FunctionNameAttribute).Assembly;
71+
_assemblyNameToObjMap[WebJobsHostAssemblyName] = typeof(Microsoft.Azure.WebJobs.JobHost).Assembly;
72+
73+
// JSON.Net?
74+
}
75+
76+
// Produce tooling object
77+
var webjobsStartups = new List<Type>();
78+
foreach (var path in _assemblyNameToPathMap.Values)
79+
{
80+
// We don't want to load and reflect over every dll.
81+
// By convention, restrict based on filenames.
82+
var filename = Path.GetFileName(path);
83+
// TODO: Can this search be narrowed, e.g. "webjobs.extensions"?
84+
if (!filename.ToLowerInvariant().Contains("extension"))
85+
{
86+
continue;
87+
}
88+
if (path.Contains(@"\ref\")) // Skip reference assemblies.
89+
{
90+
continue;
91+
}
92+
93+
Assembly assembly;
94+
try
95+
{
96+
// See GetNuGetPackagesPath for details
97+
// Script runtime is already setup with assembly resolution hooks, so use LoadFrom
98+
assembly = Assembly.LoadFrom(path);
99+
100+
string asmName = new AssemblyName(assembly.FullName).Name;
101+
_assemblyNameToObjMap[asmName] = assembly;
102+
103+
var test = assembly.GetCustomAttributes<WebJobsStartupAttribute>().Select(a => a.WebJobsStartupType);
104+
if (test.Count() > 0)
105+
{
106+
webjobsStartups.AddRange(test);
107+
}
108+
}
109+
catch (Exception e)
110+
{
111+
// Could be a reference assembly.
112+
continue;
113+
}
114+
}
115+
116+
var host = new HostBuilder()
117+
.ConfigureWebJobs(b =>
118+
{
119+
b.AddAzureStorageCoreServices()
120+
.UseExternalStartup(new CompilationWebJobsStartupTypeLocator(_assemblyNameToObjMap.Values.ToArray()));
121+
})
122+
.Build();
123+
var tooling = (JobHostMetadataProvider)host.Services.GetRequiredService<IJobHostMetadataProvider>();
124+
125+
lock (this)
126+
{
127+
this._projectCount = count;
128+
this._tooling = tooling;
129+
}
130+
}
131+
132+
public bool TryMapAssembly(IAssemblySymbol asm, out Assembly asmRef)
133+
{
134+
// Top-level map only supports mscorlib, webjobs, or extensions
135+
var asmName = asm.Identity.Name;
136+
137+
Assembly asm2;
138+
if (_assemblyNameToObjMap.TryGetValue(asmName, out asm2))
139+
{
140+
asmRef = asm2;
141+
return true;
142+
}
143+
144+
// Is this an extension? Must have a reference to WebJobs
145+
bool isWebJobsAssembly = false;
146+
foreach (var module in asm.Modules)
147+
{
148+
foreach (var asmReference in module.ReferencedAssemblies)
149+
{
150+
if (asmReference.Name == WebJobsAssemblyName)
151+
{
152+
isWebJobsAssembly = true;
153+
goto Done;
154+
}
155+
}
156+
}
157+
Done:
158+
if (!isWebJobsAssembly)
159+
{
160+
asmRef = null;
161+
return false;
162+
}
163+
164+
foreach (var kv in _assemblyNameToPathMap)
165+
{
166+
var path = kv.Value;
167+
var shortName = Path.GetFileNameWithoutExtension(path);
168+
169+
if (string.Equals(asmName, shortName, StringComparison.OrdinalIgnoreCase))
170+
{
171+
var asm3 = Assembly.LoadFile(path);
172+
_assemblyNameToObjMap[asmName] = asm3;
173+
174+
asmRef = asm3;
175+
return true;
176+
}
177+
}
178+
179+
throw new NotImplementedException();
180+
}
181+
182+
public void Register()
183+
{
184+
if (_registered)
185+
{
186+
return;
187+
}
188+
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
189+
_registered = true;
190+
}
191+
192+
private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
193+
{
194+
var an = new AssemblyName(args.Name);
195+
var context = args.RequestingAssembly;
196+
197+
Assembly asm2;
198+
if (_assemblyNameToObjMap.TryGetValue(an.Name, out asm2))
199+
{
200+
return asm2;
201+
}
202+
203+
asm2 = LoadFromProjectReference(an);
204+
if (asm2 != null)
205+
{
206+
_assemblyNameToObjMap[an.Name] = asm2;
207+
}
208+
209+
return asm2;
210+
}
211+
212+
private Assembly LoadFromProjectReference(AssemblyName an)
213+
{
214+
foreach (var kv in _assemblyNameToPathMap)
215+
{
216+
var path = kv.Key;
217+
if (path.Contains(@"\ref\")) // Skip reference assemblies.
218+
{
219+
continue;
220+
}
221+
222+
var filename = Path.GetFileNameWithoutExtension(path);
223+
224+
// Simplifying assumption: assume dll name matches assembly name.
225+
// Use this as a filter to limit the number of file-touches.
226+
if (string.Equals(filename, an.Name, StringComparison.OrdinalIgnoreCase))
227+
{
228+
var an2 = AssemblyName.GetAssemblyName(path);
229+
230+
if (string.Equals(an2.FullName, an.FullName, StringComparison.OrdinalIgnoreCase))
231+
{
232+
var a = Assembly.LoadFrom(path);
233+
return a;
234+
}
235+
}
236+
}
237+
return null;
238+
}
239+
}
240+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Linq;
6+
using System.Reflection;
7+
using Microsoft.Azure.WebJobs.Hosting;
8+
9+
namespace Microsoft.Azure.Functions.Analyzers
10+
{
11+
public class CompilationWebJobsStartupTypeLocator : IWebJobsStartupTypeLocator
12+
{
13+
private readonly Assembly[] _assemblies;
14+
private readonly Lazy<Type[]> _startupTypes;
15+
16+
public CompilationWebJobsStartupTypeLocator()
17+
{
18+
_startupTypes = new Lazy<Type[]>(GetTypes);
19+
}
20+
21+
internal CompilationWebJobsStartupTypeLocator(Assembly[] assemblies)
22+
: this()
23+
{
24+
_assemblies = assemblies;
25+
}
26+
27+
public Type[] GetStartupTypes()
28+
{
29+
var types = _startupTypes.Value;
30+
return _startupTypes.Value;
31+
}
32+
33+
public Type[] GetTypes()
34+
{
35+
return _assemblies?.SelectMany(a => a.GetCustomAttributes<WebJobsStartupAttribute>().Select(a => a.WebJobsStartupType)).ToArray();
36+
}
37+
}
38+
}

src/WebJobs.Script.Analyzers/DiagnosticDescriptors.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,17 @@ private static DiagnosticDescriptor Create(string id, string title, string messa
3030
messageFormat: "Function name can't be '{0}'. It must start with a letter, be no longer than 128 characters, and contain only alphanumeric characters, underscores, or hyphens.",
3131
category: Constants.DiagnosticsCategories.WebJobsBindings,
3232
severity: DiagnosticSeverity.Error);
33+
34+
public static DiagnosticDescriptor BadBindingExpressionSyntax { get; }
35+
= Create(id: "AZF0004", title: "Illegal binding expression syntax",
36+
messageFormat: "{0} can't be value '{1}': {2}",
37+
category: Constants.DiagnosticsCategories.WebJobsBindings,
38+
severity: DiagnosticSeverity.Warning);
39+
40+
public static DiagnosticDescriptor FailedValidation { get; }
41+
= Create(id: "AZF0005", title: "Illegal binding type",
42+
messageFormat: "{0} can't be value '{1}': {2}",
43+
category: Constants.DiagnosticsCategories.WebJobsBindings,
44+
severity: DiagnosticSeverity.Warning);
3345
}
3446
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.Azure.Functions.Analyzers
7+
{
8+
class ArrayType : FakeType
9+
{
10+
private readonly Type _inner;
11+
public ArrayType(Type inner)
12+
: base(null, null, null)
13+
{
14+
_inner = inner;
15+
}
16+
17+
protected override bool IsArrayImpl()
18+
{
19+
return true;
20+
}
21+
22+
public override Type GetElementType()
23+
{
24+
return _inner;
25+
}
26+
27+
public override string ToString()
28+
{
29+
return _inner + "[]";
30+
}
31+
}
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.Azure.Functions.Analyzers
7+
{
8+
class ByRefType : FakeType
9+
{
10+
private readonly Type _inner;
11+
public ByRefType(Type inner)
12+
: base(null, null, null)
13+
{
14+
_inner = inner;
15+
}
16+
17+
protected override bool IsByRefImpl()
18+
{
19+
return true;
20+
}
21+
22+
public override Type GetElementType()
23+
{
24+
return _inner;
25+
}
26+
27+
public override string ToString()
28+
{
29+
return _inner.ToString() + "&";
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)