Skip to content

Commit

Permalink
Cache the processing of EditorConfigFile. (#23)
Browse files Browse the repository at this point in the history
Co-authored-by: Martijn Laarman <[email protected]>
  • Loading branch information
nojaf and Mpdreamz authored Jun 2, 2023
1 parent 36595c8 commit ab20e5e
Show file tree
Hide file tree
Showing 12 changed files with 134 additions and 30 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ on:

jobs:
build:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/verify.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on: [push, pull_request]

jobs:
linux:
runs-on: ubuntu-18.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
Expand Down
4 changes: 0 additions & 4 deletions src/EditorConfig.App/ArgumentsParser.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EditorConfig.App
{
Expand Down
1 change: 0 additions & 1 deletion src/EditorConfig.App/EditorConfig.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>EditorConfig.App</AssemblyName>
<RootNamespace>ReleaseNotes</RootNamespace>
<PackAsTool>true</PackAsTool>
<ToolCommandName>editorconfig</ToolCommandName>
<PackageId>editorconfig-tool</PackageId>
Expand Down
13 changes: 6 additions & 7 deletions src/EditorConfig.Core/ConfigSection.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace EditorConfig.Core
{
Expand All @@ -11,18 +10,18 @@ public class ConfigSection : IReadOnlyDictionary<string, string>
{
private readonly Dictionary<string, string> _backingDictionary;

private static readonly Dictionary<string, string> DefaultGlobalDictionary = new Dictionary<string, string>();
/// <summary> Represents an ini section within the editorconfig file </summary>
public ConfigSection() => _backingDictionary = DefaultGlobalDictionary;

/// <summary> Represents an ini section within the editorconfig file </summary>
public ConfigSection(string name, string configDirectory, Dictionary<string, string> backingDictionary)
public ConfigSection(string name, IEditorConfigFile origin, Dictionary<string, string> backingDictionary)
{
Glob = FixGlob(name, configDirectory);
EditorConfigFile = origin;
Glob = FixGlob(name, origin.Directory);
_backingDictionary = backingDictionary ?? new Dictionary<string, string>();
ParseKnownProperties();
}

/// <summary> Originating <see cref="EditorConfigFile"/> </summary>
public IEditorConfigFile EditorConfigFile { get; }

/// <summary> The glob pattern this section describes</summary>
public string Glob { get; }

Expand Down
47 changes: 35 additions & 12 deletions src/EditorConfig.Core/EditorConfigFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,31 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace EditorConfig.Core
{
/// <summary> Information about the originating editor config file</summary>
public interface IEditorConfigFile
{
/// <summary> The directory of the EditorConfig file </summary>
public string Directory { get; }

/// <summary> The name of the EditorConfig file </summary>
public string FileName { get; }

/// <summary> A hint this instance of <see cref="EditorConfigFile"/> was cached</summary>
public string CacheKey { get; }

/// <summary> Indicates wheter the loaded editorconfig represents the root of the chain </summary>
public bool IsRoot { get; }
}


/// <summary>
/// Represents the raw config file as INI, please use <see cref="EditorConfigParser.GetConfigurationFilesTillRoot"/>
/// </summary>
public class EditorConfigFile
public class EditorConfigFile : IEditorConfigFile
{
private static readonly Regex SectionRe = new Regex(@"^\s*\[(([^#;]|\\#|\\;)+)\]\s*([#;].*)?$");
private static readonly Regex CommentRe = new Regex(@"^\s*[#;]");
Expand All @@ -33,21 +49,28 @@ public class EditorConfigFile
/// <summary> All discovered sections </summary>
public List<ConfigSection> Sections { get; } = new List<ConfigSection>();

/// <summary> The directory of the EditorConfig file </summary>
/// <inheritdoc cref="IEditorConfigFile.Directory"/>
public string Directory { get; }

/// <inheritdoc cref="IEditorConfigFile.FileName"/>
public string FileName { get; }

/// <inheritdoc cref="IEditorConfigFile.CacheKey"/>
public string CacheKey { get; }

private readonly bool _isRoot;
/// <summary> Indicates wheter the loaded editorconfig represents the root of the chain </summary>
/// <inheritdoc cref="IEditorConfigFile.IsRoot"/>
public bool IsRoot => _isRoot;

internal EditorConfigFile(string file)
internal EditorConfigFile(string path, string cacheKey = null)
{
Directory = Path.GetDirectoryName(file);
Parse(file);

if (_globalDict.ContainsKey("root"))
bool.TryParse(_globalDict["root"], out _isRoot);
Directory = Path.GetDirectoryName(path);
FileName = Path.GetFileName(path);
CacheKey = cacheKey;
Parse(path);

if (_globalDict.TryGetValue("root", out var value))
bool.TryParse(value, out _isRoot);
}

private void Parse(string file)
Expand Down Expand Up @@ -82,7 +105,7 @@ private void Parse(string file)

if (!string.IsNullOrEmpty(sectionName))
{
var section = new ConfigSection(sectionName, Directory, activeDict);
var section = new ConfigSection(sectionName, this, activeDict);
Sections.Add(section);
reset = true;
}
Expand All @@ -93,7 +116,7 @@ private void Parse(string file)

if (!reset)
{
var section = new ConfigSection(sectionName, Directory, activeDict);
var section = new ConfigSection(sectionName, this, activeDict);
Sections.Add(section);
}

Expand Down
36 changes: 36 additions & 0 deletions src/EditorConfig.Core/EditorConfigFileCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Collections.Concurrent;
using System.IO;

namespace EditorConfig.Core;

/// <summary>
/// Cache unchanged parsed EditorConfigFiles.
/// </summary>
public static class EditorConfigFileCache
{
private static string GetFileHash(string filename)
{
using var sha256 = System.Security.Cryptography.SHA256.Create();
using var stream = File.OpenRead(filename);
var hash = sha256.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "");
}

private static readonly ConcurrentDictionary<string, EditorConfigFile> FileCache = new();

/// <summary>
/// Retrieves a cached EditorConfigFile based on the file name and file hash.
/// The cache will be populated when the file was not present.
/// </summary>
/// <remarks>This function is thread safe. The cache will not be hit when the file does not exist.</remarks>
/// <param name="file"></param>
/// <returns></returns>
public static EditorConfigFile GetOrCreate(string file)
{
if (!File.Exists(file)) return new EditorConfigFile(file);

var key = $"{file}_{GetFileHash(file)}";
return FileCache.GetOrAdd(key, _ => new EditorConfigFile(file));
}
}
21 changes: 19 additions & 2 deletions src/EditorConfig.Core/EditorConfigParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;

namespace EditorConfig.Core
{
Expand All @@ -12,6 +11,8 @@ namespace EditorConfig.Core
/// </summary>
public class EditorConfigParser
{
private Func<string, EditorConfigFile> Factory { get; }

/// <summary>
/// The current (and latest parser supported) version as string
/// </summary>
Expand Down Expand Up @@ -40,7 +41,23 @@ public class EditorConfigParser
/// <param name="configFileName">The name of the file(s) holding the editorconfiguration values</param>
/// <param name="developmentVersion">Only used in testing, development to pass an older version to the parsing routine</param>
public EditorConfigParser(string configFileName = ".editorconfig", Version developmentVersion = null)
: this(f => new EditorConfigFile(f), configFileName, developmentVersion)
{

}

/// <summary>
/// The EditorConfigParser locates all relevant editorconfig files and makes sure they are merged correctly.
/// </summary>
/// <param name="factory">
/// Function that take the file name and constructs a new EditorConfigFile instance.
/// Pass `EditorConfigFileCache.GetOrCreate` to apply caching.
/// </param>
/// <param name="configFileName"></param>
/// <param name="developmentVersion"></param>
public EditorConfigParser(Func<string, EditorConfigFile> factory, string configFileName = ".editorconfig", Version developmentVersion = null)
{
Factory = factory;
ConfigFileName = configFileName ?? ".editorconfig";
ParseVersion = developmentVersion ?? Version;
}
Expand Down Expand Up @@ -102,7 +119,7 @@ public IList<EditorConfigFile> GetConfigurationFilesTillRoot(string file)

private IEnumerable<EditorConfigFile> ParseConfigFilesTillRoot(IEnumerable<string> configFiles)
{
foreach (var configFile in configFiles.Select(f=> new EditorConfigFile(f)))
foreach (var configFile in configFiles.Select(Factory))
{
yield return configFile;
if (configFile.IsRoot) yield break;
Expand Down
3 changes: 1 addition & 2 deletions src/EditorConfig.Core/EditorConfigWorkspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ public EditorConfigWorkspace(FileInfo rootEditorConfigFile, string configFileNam
var directory = rootEditorConfigFile.Directory;
_editorconfigFiles =
directory.EnumerateFiles(configFileName, SearchOption.AllDirectories)
.Select(d=>new EditorConfigFile(d.Name));

.Select(d=> EditorConfigFileCache.GetOrCreate(d.Name));
}


Expand Down
6 changes: 6 additions & 0 deletions src/EditorConfig.Core/FileConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ public class FileConfiguration
/// </summary>
public Version Version { get; }

/// <summary>
/// Editorconfig files that were used to construct this instance of <see cref="FileConfiguration"/>
/// </summary>
public IReadOnlyCollection<IEditorConfigFile> EditorConfigFiles { get; }

/// <summary>
/// Holds the editor configuration for a file, please use <see cref="EditorConfigParser.Parse(string[])"/> to get an instance
/// </summary>
Expand All @@ -79,6 +84,7 @@ internal FileConfiguration(Version version, string fileName, List<ConfigSection>
if (version == null) throw new ArgumentNullException(nameof(version));
if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("file should not be null or whitespace", nameof(fileName));

EditorConfigFiles = sections.Select(s => s.EditorConfigFile).ToArray();
FileName = fileName;
Version = version;
Sections = sections;
Expand Down
4 changes: 4 additions & 0 deletions src/EditorConfig.Tests/Caching/.editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
root = true

[*]
end_of_line = lf
25 changes: 25 additions & 0 deletions src/EditorConfig.Tests/Caching/CachingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Reflection;
using EditorConfig.Core;
using FluentAssertions;
using NUnit.Framework;

namespace EditorConfig.Tests.Caching
{
[TestFixture]
public class CachingTests : EditorConfigTestBase
{
[Test]
public void FileShouldCached()
{
var fileName = GetFileFromMethod(MethodBase.GetCurrentMethod(), ".editorconfig");

var parser = new EditorConfigParser(EditorConfigFileCache.GetOrCreate);
var config1 = parser.Parse(fileName);
config1.EditorConfigFiles.Should().NotBeNullOrEmpty();
config1.EditorConfigFiles.Should().OnlyContain(f => !string.IsNullOrEmpty(f.CacheKey));
var config2 = parser.Parse(fileName);
config2.EditorConfigFiles.Should().NotBeNullOrEmpty();
config2.EditorConfigFiles.Should().OnlyContain(f => !string.IsNullOrEmpty(f.CacheKey));
}
}
}

0 comments on commit ab20e5e

Please sign in to comment.