Skip to content

Commit

Permalink
Use a more comprehensible vector data structure
Browse files Browse the repository at this point in the history
  • Loading branch information
Sylvan Mably committed Mar 13, 2017
1 parent 862aecc commit cb2f574
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 94 deletions.
39 changes: 20 additions & 19 deletions lib/config.js
Expand Up @@ -268,35 +268,36 @@ class Config {
}

/**
* Gets the vector of applicable configs from the hierarchy for a given file, including any overrides that apply to
* the specified file path. A vector is an array of config file paths, each optionally followed by one or more
* numbers that correspond to the indices of nested config blocks within the config file's overrides section whose
* glob patterns match the specified file path; e.g., the vector ['/home/john/project/app/.eslintrc', 0, 2] would
* indicate that the main .eslintrc file and its first and third override blocks apply to the current file.
* Gets the vector of applicable configs and subconfigs from the hierarchy for a given file. A vector is an array of
* objects, each containing a config file path and an array of override indices corresponding to entries in config
* file's overrides section whose glob patterns match the specified file path; e.g., the vector
* { configFile: '/home/john/app/.eslintrc', matchingOverrides: [0, 2] } would indicate that the main project
* .eslintrc file and its first and third override blocks apply to the current file.
* @param {string} filePath The file path for which to build the hierarchy and config vector.
* @returns {Array<number|string>} array of config file paths or nested override indices
* @returns {Array<Object>} config vector applicable to the specified path
* @private
*/
getConfigVector(filePath) {
const directory = filePath ? path.dirname(filePath) : this.options.cwd,
vector = [];

this.getConfigHierarchy(directory).forEach(config => {
const overrides = config.overrides;

vector.push(config.filePath);

if (!overrides) {
return;
const vectorEntry = {
filePath: config.filePath,
matchingOverrides: []
};

if (config.overrides) {
const relativePath = path.relative(config.baseDirectory, filePath || directory);

config.overrides.forEach((override, i) => {
if (ConfigOps.pathMatchesGlobs(relativePath, override.files)) {
vectorEntry.matchingOverrides.push(i);
}
});
}

const relativePath = path.relative(config.baseDirectory, filePath || directory);

overrides.forEach((override, i) => {
if (ConfigOps.pathMatchesGlobs(relativePath, override.files)) {
vector.push(i);
}
});
vector.push(vectorEntry);
});

return vector;
Expand Down
28 changes: 21 additions & 7 deletions lib/config/config-cache.js
Expand Up @@ -16,13 +16,27 @@ const VECTOR_SEP = ",";
//------------------------------------------------------------------------------

/**
* Get a string hash for a vector object
* @param {Array<number|string>} vector config vector to hash
* Get a string hash for a single object from a config vector
* @param {Object} entry config vector entry
* @returns {string} hash of the vector entry
* @private
*/
function hashVectorEntry(entry) {
return [].concat(entry.filePath, entry.matchingOverrides).join(VECTOR_SEP);
}

/**
* Get a string hash for a config vector
* @param {Array<Object>} vector config vector to hash
* @returns {string} hash of the vector values
* @private
*/
function hash(vector) {
return vector ? vector.join(VECTOR_SEP) : "";
if (!vector) {
return "";
}

return vector.map(hashVectorEntry).join(VECTOR_SEP);
}

//------------------------------------------------------------------------------
Expand Down Expand Up @@ -102,7 +116,7 @@ class ConfigCache {

/**
* Gets a merged config object corresponding to the supplied vector.
* @param {Array<number|string>} vector the vector to find a merged config for
* @param {Array<Object>} vector the vector to find a merged config for
* @returns {Object|null} a merged config object, if found in the cache, otherwise null
* @private
*/
Expand All @@ -112,7 +126,7 @@ class ConfigCache {

/**
* Sets a merged config object in the cache for the supplied vector.
* @param {Array<number|string>} vector the vector to save a merged config for
* @param {Array<Object>} vector the vector to save a merged config for
* @param {Object} config the merged config object to add to the cache
* @returns {void}
* @private
Expand All @@ -124,7 +138,7 @@ class ConfigCache {
/**
* Gets a merged config object corresponding to the supplied vector, including configuration options from outside
* the vector.
* @param {Array<number|string>} vector the vector to find a merged config for
* @param {Array<Object>} vector the vector to find a merged config for
* @returns {Object|null} a merged config object, if found in the cache, otherwise null
* @private
*/
Expand All @@ -135,7 +149,7 @@ class ConfigCache {
/**
* Sets a merged config object in the cache for the supplied vector, including configuration options from outside
* the vector.
* @param {Array<number|string>} vector the vector to save a merged config for
* @param {Array<Object>} vector the vector to save a merged config for
* @param {Object} config the merged config object to add to the cache
* @returns {void}
* @private
Expand Down
100 changes: 56 additions & 44 deletions lib/config/config-ops.js
Expand Up @@ -276,72 +276,84 @@ module.exports = {
},

/**
* Merges all configurations in a given config vector. A vector is an array of config file paths, each optionally
* followed by one or more numbers that correspond to the indices of nested config blocks within the config file's
* overrides section. All config data is assumed to be cached.
* @param {Array<number|string>} vector array of config file paths or relative override indices
* Merges all configurations in a given config vector. A vector is an array of objects, each containing a config
* file path and a list of subconfig indices that match the current file path. All config data is assumed to be
* cached.
* @param {Array<Object>} vector list of config files and their subconfig indices that match the current file path
* @returns {Object} config object
*/
getConfigFromVector(vector) {

const cachedConfig = configCache.getMergedVectorConfig(vector);

if (cachedConfig) {
return cachedConfig;
}

debug("Using config from partial cache");

const subvector = Array.from(vector);
let config,
subConfigs,
nearestCacheIndex = vector.length - 1;
let nearestCacheIndex = subvector.length - 1,
partialCachedConfig;

// Check the merged vector config cache to try to find a merged config for the current config or a parent
while (nearestCacheIndex >= 0) {
config = configCache.getMergedVectorConfig(subvector);
if (config) {
partialCachedConfig = configCache.getMergedVectorConfig(subvector);
if (partialCachedConfig) {
break;
}
subvector.pop();
nearestCacheIndex--;
}

if (config) {
if (nearestCacheIndex === vector.length - 1) {
return config;
}
debug("Using config from partial cache");
if (!partialCachedConfig) {
partialCachedConfig = {};
}

// Get parent config if configKey is an override index
if (typeof vector[nearestCacheIndex + 1] === "number") {
let finalConfig = partialCachedConfig;

// If the first non-cached vector is an override index, subConfigs needs to be set
let parentConfigKey = vector[nearestCacheIndex];
// Start from entry immediately following nearest cached config (first uncached entry)
for (let i = nearestCacheIndex + 1; i < vector.length; i++) {
finalConfig = this.mergeVectorEntry(finalConfig, vector[i]);
configCache.setMergedVectorConfig(vector.slice(0, i + 1), finalConfig);
}

for (let i = nearestCacheIndex - 1; i >= 0 && typeof parentConfigKey !== "string"; i--) {
parentConfigKey = vector[i];
}
subConfigs = configCache.getConfig(parentConfigKey).overrides;
}
return finalConfig;
},

/**
* Merges the config options from a single vector entry into the supplied config.
* @param {Object} config the base config to merge the vector entry's options into
* @param {Object} vectorEntry a single entry from a vector, consisting of a config file path and an array of
* matching override indices
* @returns {Object} merged config object
*/
mergeVectorEntry(config, vectorEntry) {
const vectorEntryConfig = Object.assign({}, configCache.getConfig(vectorEntry.filePath));
let mergedConfig = Object.assign({}, config),
overrides;

if (vectorEntryConfig.overrides) {
overrides = vectorEntryConfig.overrides.filter(
(override, overrideIndex) => vectorEntry.matchingOverrides.includes(overrideIndex)
);
} else {
config = {};
overrides = [];
}

// Start from index of nearest cached config
for (let i = nearestCacheIndex + 1; i < vector.length; i++) {
const configKey = vector[i];
let shallowConfig;
mergedConfig = this.merge(mergedConfig, vectorEntryConfig);

if (typeof configKey === "string") {
shallowConfig = configCache.getConfig(configKey);
subConfigs = shallowConfig.overrides;
} else {
shallowConfig = subConfigs[configKey];
}
config = this.merge(config, shallowConfig);
if (config.filePath) {
delete config.filePath;
delete config.baseDirectory;
} else if (config.files) {
delete config.files;
}
configCache.setMergedVectorConfig(vector, config);
delete mergedConfig.overrides;

mergedConfig = overrides.reduce((lastConfig, override) => this.merge(lastConfig, override), mergedConfig);

if (mergedConfig.filePath) {
delete mergedConfig.filePath;
delete mergedConfig.baseDirectory;
} else if (mergedConfig.files) {
delete mergedConfig.files;
}

return config;
return mergedConfig;
},

/**
Expand Down
32 changes: 8 additions & 24 deletions tests/lib/config/config-ops.js
Expand Up @@ -873,7 +873,10 @@ describe("ConfigOps", () => {
});

it("should get from merged vector cache when present", () => {
const vector = ["configFile1", 1, "configFile2", 0, 1];
const vector = [
{ filePath: "configFile1", matchingOverrides: [1] },
{ filePath: "configFile2", matchingOverrides: [0, 1] }
];
const merged = { merged: true };

configCache.setMergedVectorConfig(vector, merged);
Expand All @@ -883,28 +886,6 @@ describe("ConfigOps", () => {
assert.deepEqual(result, merged);
});

it("should get from correct partial merged vector cache", () => {
const vector1 = ["configFile1", 1, "configFile2", 0, 1];

const vector2 = ["configFile1", 1, "configFile2", 0];
const merged2 = { mergedLevel2: true };

const vector3 = ["configFile1", 1];
const merged3 = { mergedLevel3: true };

const config2 = { overrides: [{}, { files: "pattern2", rules: { foo2: "off" } }] };

configCache.setMergedVectorConfig(vector2, merged2);
configCache.setMergedVectorConfig(vector3, merged3);
configCache.setConfig("configFile2", config2);

const result = ConfigOps.getConfigFromVector(vector1);

assert.isTrue(result.mergedLevel2);
assert.isUndefined(result.mergedLevel3);
assert.equal(result.rules.foo2, "off");
});

it("should get from raw cached configs when no merged vectors are cached", () => {
const config = [
{
Expand All @@ -926,7 +907,10 @@ describe("ConfigOps", () => {
configCache.setConfig("configFile1", config[0]);
configCache.setConfig("configFile2", config[1]);

const vector = ["configFile1", 1, "configFile2", 0, 1];
const vector = [
{ filePath: "configFile1", matchingOverrides: [1] },
{ filePath: "configFile2", matchingOverrides: [0, 1] }
];

const result = ConfigOps.getConfigFromVector(vector);

Expand Down

0 comments on commit cb2f574

Please sign in to comment.