ember-template-lint
Version:
Linter for Ember or Handlebars templates.
243 lines (234 loc) • 7.42 kB
JavaScript
// main code parts copypasted from https://github.com/editorconfig/editorconfig-core-js and and rewritten as Class.
'use strict';
const fs = require('fs');
const path = require('path');
const micromatch = require('micromatch');
class EditorConfigResolver {
constructor(workingDir) {
this.workingDir = workingDir;
this.EDITOR_CONFIG_CACHE = null;
this.CONFIG_RESOLUTIONS_CACHE = new Map();
/**
* define the possible values:
* section: [section]
* param: key=value
* comment: ;this is a comment
*/
this.regex = {
section: /^\s*\[(([^#;]|\\#|\\;)+)]\s*([#;].*)?$/,
param: /^\s*([\w.-]+)\s*[:=]\s*(.*?)\s*([#;].*)?$/,
comment: /^\s*[#;].*$/,
};
this.knownProps = {
end_of_line: true,
indent_style: true,
indent_size: true,
insert_final_newline: true,
trim_trailing_whitespace: true,
charset: true,
};
}
opts(filepath, options) {
const resolvedFilePath = path.resolve(filepath);
return [resolvedFilePath, this.processOptions(options, resolvedFilePath)];
}
processOptions(options = {}, filepath) {
return {
config: options.config || '.editorconfig',
root: path.resolve(options.root || path.parse(filepath).root),
};
}
parseString(data) {
let sectionBody = {};
let sectionName = null;
const value = [[sectionName, sectionBody]];
const lines = data.split(/\r\n|\r|\n/);
for (const line of lines) {
let match;
if (this.regex.comment.test(line)) {
continue;
}
if (this.regex.param.test(line)) {
match = line.match(this.regex.param);
sectionBody[match[1]] = match[2];
} else if (this.regex.section.test(line)) {
match = line.match(this.regex.section);
sectionName = match[1];
sectionBody = {};
value.push([sectionName, sectionBody]);
}
}
return value;
}
buildFullGlob(pathPrefix, glob) {
switch (glob.indexOf('/')) {
case -1:
glob = `**/${glob}`;
break;
case 0:
glob = glob.slice(1);
break;
default:
break;
}
return path.join(pathPrefix, glob);
}
fnmatch(filepath, glob) {
const matchOptions = { matchBase: false, dot: false, noext: true };
glob = glob.split(path.sep).join('/');
filepath = filepath.split(path.sep).join('/');
return micromatch.isMatch(filepath, glob, matchOptions);
}
extendProps(props = {}, options = {}) {
for (const key in options) {
if (Object.prototype.hasOwnProperty.call(options, key)) {
const value = options[key];
const key2 = key.toLowerCase();
let value2 = value;
if (this.knownProps[key2]) {
value2 = value.toLowerCase();
}
try {
value2 = JSON.parse(value);
} catch {
//
}
if (typeof value === 'undefined' || value === null) {
// null and undefined are values specific to JSON (no special meaning
// in editorconfig) & should just be returned as regular strings.
value2 = String(value);
}
props[key2] = value2;
}
}
return props;
}
readConfigFilesSync(filepaths) {
const files = [];
for (const filepath of filepaths) {
if (!fs.existsSync(filepath)) {
continue;
}
let file;
try {
file = fs.readFileSync(filepath, 'utf8');
} catch {
file = '';
}
files.push({
name: filepath,
contents: file,
});
}
return files;
}
processMatches(matches) {
// Set indent_size to 'tab' if indent_size is unspecified and
// indent_style is set to 'tab'.
if (
'indent_style' in matches &&
matches.indent_style === 'tab' &&
!('indent_size' in matches)
) {
matches.indent_size = 'tab';
}
// Set tab_width to indent_size if indent_size is specified and
// tab_width is unspecified
if ('indent_size' in matches && !('tab_width' in matches) && matches.indent_size !== 'tab') {
matches.tab_width = matches.indent_size;
}
// Set indent_size to tab_width if indent_size is 'tab'
if ('indent_size' in matches && 'tab_width' in matches && matches.indent_size === 'tab') {
matches.indent_size = matches.tab_width;
}
return matches;
}
parseFromConfigs(configs, filepath) {
return this.processMatches(
configs.reverse().reduce((matches, file) => {
const pathPrefix = path.dirname(file.name);
for (const section of file.contents) {
const [glob, options] = section;
if (!glob) {
continue;
}
const fullGlob = this.buildFullGlob(pathPrefix, glob);
if (!this.fnmatch(filepath, fullGlob)) {
continue;
}
matches = this.extendProps(matches, options);
}
return matches;
}, {})
);
}
resetEditorConfigCache() {
this.EDITOR_CONFIG_CACHE = null;
}
resetConfigResolutionsCache() {
this.CONFIG_RESOLUTIONS_CACHE.clear();
}
getConfigFileNames(filepath, options) {
const paths = [];
do {
filepath = path.dirname(filepath);
paths.push(path.join(filepath, options.config));
} while (filepath !== options.root);
return paths;
}
getConfigsForFiles(files) {
const configs = [];
for (const i in files) {
if (Object.prototype.hasOwnProperty.call(files, i)) {
const file = files[i];
const contents = this.parseString(file.contents);
configs.push({
name: file.name,
contents,
});
if ((contents[0][1].root || '').toLowerCase() === 'true') {
break;
}
}
}
return configs;
}
/*
provcess.cwd() = /root/path
dirname(/root/path) => root
but, we need to find .editorconfig under /root/path
provcess.cwd() + "this.file.does.not.exist" = /root/path/this.file.does.not.exist
dirname(/root/path/this.file.does.not.exist) => /root/path
*/
resolveEditorConfigFiles(
_filepath = path.join(this.workingDir, 'this.file.does.not.exist'),
_options = {}
) {
const [resolvedFilePath, processedOptions] = this.opts(_filepath, _options);
const filepaths = this.getConfigFileNames(resolvedFilePath, processedOptions);
const files = this.readConfigFilesSync(filepaths);
const configForFiles = this.getConfigsForFiles(files);
return configForFiles;
}
checkEditorConfigCache(entry, _opts) {
if (this.EDITOR_CONFIG_CACHE === null) {
this.EDITOR_CONFIG_CACHE = this.resolveEditorConfigFiles(entry, _opts);
}
}
editorConfigForFilePath(resolvedFilePath) {
return this.parseFromConfigs(this.EDITOR_CONFIG_CACHE, resolvedFilePath);
}
cachedEditorConfigForFilePath(resolvedFilePath) {
if (!this.CONFIG_RESOLUTIONS_CACHE.has(resolvedFilePath)) {
const editorConfig = this.editorConfigForFilePath(resolvedFilePath);
this.CONFIG_RESOLUTIONS_CACHE.set(resolvedFilePath, JSON.stringify(editorConfig));
}
return JSON.parse(this.CONFIG_RESOLUTIONS_CACHE.get(resolvedFilePath));
}
getEditorConfigData(entry, _opts = {}) {
this.checkEditorConfigCache(entry, _opts);
const resolvedFilePath = path.resolve(entry);
return this.cachedEditorConfigForFilePath(resolvedFilePath);
}
}
module.exports = EditorConfigResolver;