UNPKG

6.91 kBPlain TextView Raw
1import * as fs from 'fs'
2import * as path from 'path'
3import * as semver from 'semver'
4
5import minimatch from './lib/fnmatch'
6import { parseString, ParseStringResult } from './lib/ini'
7
8export { parseString }
9
10import pkg from '../package.json'
11
12export interface KnownProps {
13 end_of_line?: 'lf' | 'crlf' | 'unset'
14 indent_style?: 'tab' | 'space' | 'unset'
15 indent_size?: number | 'tab' | 'unset'
16 insert_final_newline?: true | false | 'unset'
17 tab_width?: number | 'unset'
18 trim_trailing_whitespace?: true | false | 'unset'
19 charset?: string | 'unset'
20}
21
22export interface ECFile {
23 name: string
24 contents: string | Buffer
25}
26
27export interface FileConfig {
28 name: string
29 contents: ParseStringResult
30}
31
32export interface ParseOptions {
33 config?: string
34 version?: string
35 root?: string
36}
37
38const knownProps = {
39 end_of_line: true,
40 indent_style: true,
41 indent_size: true,
42 insert_final_newline: true,
43 trim_trailing_whitespace: true,
44 charset: true,
45}
46
47function fnmatch(filepath: string, glob: string) {
48 const matchOptions = { matchBase: true, dot: true, noext: true }
49 glob = glob.replace(/\*\*/g, '{*,**/**/**}')
50 return minimatch(filepath, glob, matchOptions)
51}
52
53function getConfigFileNames(filepath: string, options: ParseOptions) {
54 const paths = []
55 do {
56 filepath = path.dirname(filepath)
57 paths.push(path.join(filepath, options.config as string))
58 } while (filepath !== options.root)
59 return paths
60}
61
62function processMatches(matches: KnownProps, version: string) {
63 // Set indent_size to 'tab' if indent_size is unspecified and
64 // indent_style is set to 'tab'.
65 if (
66 'indent_style' in matches
67 && matches.indent_style === 'tab'
68 && !('indent_size' in matches)
69 && semver.gte(version, '0.10.0')
70 ) {
71 matches.indent_size = 'tab'
72 }
73
74 // Set tab_width to indent_size if indent_size is specified and
75 // tab_width is unspecified
76 if (
77 'indent_size' in matches
78 && !('tab_width' in matches)
79 && matches.indent_size !== 'tab'
80 ) {
81 matches.tab_width = matches.indent_size
82 }
83
84 // Set indent_size to tab_width if indent_size is 'tab'
85 if (
86 'indent_size' in matches
87 && 'tab_width' in matches
88 && matches.indent_size === 'tab'
89 ) {
90 matches.indent_size = matches.tab_width
91 }
92
93 return matches
94}
95
96function processOptions(options: ParseOptions = {}, filepath: string) {
97 return {
98 config: options.config || '.editorconfig',
99 version: options.version || pkg.version,
100 root: path.resolve(options.root || path.parse(filepath).root),
101 }
102}
103
104function buildFullGlob(pathPrefix: string, glob: string) {
105 switch (glob.indexOf('/')) {
106 case -1:
107 glob = '**/' + glob
108 break
109 case 0:
110 glob = glob.substring(1)
111 break
112 default:
113 break
114 }
115 return path.join(pathPrefix, glob)
116}
117
118function extendProps(props: {} = {}, options: {} = {}) {
119 for (const key in options) {
120 if (options.hasOwnProperty(key)) {
121 const value = options[key]
122 const key2 = key.toLowerCase()
123 let value2 = value
124 if (knownProps[key2]) {
125 value2 = value.toLowerCase()
126 }
127 try {
128 value2 = JSON.parse(value)
129 } catch (e) {}
130 if (typeof value === 'undefined' || value === null) {
131 // null and undefined are values specific to JSON (no special meaning
132 // in editorconfig) & should just be returned as regular strings.
133 value2 = String(value)
134 }
135 props[key2] = value2
136 }
137 }
138 return props
139}
140
141function parseFromConfigs(
142 configs: FileConfig[],
143 filepath: string,
144 options: ParseOptions,
145) {
146 return processMatches(
147 configs
148 .reverse()
149 .reduce(
150 (matches: KnownProps, file) => {
151 const pathPrefix = path.dirname(file.name)
152 file.contents.forEach((section) => {
153 const glob = section[0]
154 const options2 = section[1]
155 if (!glob) {
156 return
157 }
158 const fullGlob = buildFullGlob(pathPrefix, glob)
159 if (!fnmatch(filepath, fullGlob)) {
160 return
161 }
162 matches = extendProps(matches, options2)
163 })
164 return matches
165 },
166 {},
167 ),
168 options.version as string,
169 )
170}
171
172function getConfigsForFiles(files: ECFile[]) {
173 const configs = []
174 for (const i in files) {
175 if (files.hasOwnProperty(i)) {
176 const file = files[i]
177 const contents = parseString(file.contents as string)
178 configs.push({
179 name: file.name,
180 contents,
181 })
182 if ((contents[0][1].root || '').toLowerCase() === 'true') {
183 break
184 }
185 }
186 }
187 return configs
188}
189
190async function readConfigFiles(filepaths: string[]) {
191 return Promise.all(
192 filepaths.map((name) => new Promise((resolve) => {
193 fs.readFile(name, 'utf8', (err, data) => {
194 resolve({
195 name,
196 contents: err ? '' : data,
197 })
198 })
199 })),
200 )
201}
202
203function readConfigFilesSync(filepaths: string[]) {
204 const files: ECFile[] = []
205 let file: string | number | Buffer
206 filepaths.forEach((filepath) => {
207 try {
208 file = fs.readFileSync(filepath, 'utf8')
209 } catch (e) {
210 file = ''
211 }
212 files.push({
213 name: filepath,
214 contents: file,
215 })
216 })
217 return files
218}
219
220function opts(filepath: string, options: ParseOptions = {}): [
221 string,
222 ParseOptions
223] {
224 const resolvedFilePath = path.resolve(filepath)
225 return [
226 resolvedFilePath,
227 processOptions(options, resolvedFilePath),
228 ]
229}
230
231export async function parseFromFiles(
232 filepath: string,
233 files: Promise<ECFile[]>,
234 options: ParseOptions = {},
235) {
236 const [resolvedFilePath, processedOptions] = opts(filepath, options)
237 return files.then(getConfigsForFiles)
238 .then((configs) => parseFromConfigs(
239 configs,
240 resolvedFilePath,
241 processedOptions,
242 ))
243}
244
245export function parseFromFilesSync(
246 filepath: string,
247 files: ECFile[],
248 options: ParseOptions = {},
249) {
250 const [resolvedFilePath, processedOptions] = opts(filepath, options)
251 return parseFromConfigs(
252 getConfigsForFiles(files),
253 resolvedFilePath,
254 processedOptions,
255 )
256}
257
258export async function parse(_filepath: string, _options: ParseOptions = {}) {
259 const [resolvedFilePath, processedOptions] = opts(_filepath, _options)
260 const filepaths = getConfigFileNames(resolvedFilePath, processedOptions)
261 return readConfigFiles(filepaths)
262 .then(getConfigsForFiles)
263 .then((configs) => parseFromConfigs(
264 configs,
265 resolvedFilePath,
266 processedOptions,
267 ))
268}
269
270export function parseSync(_filepath: string, _options: ParseOptions = {}) {
271 const [resolvedFilePath, processedOptions] = opts(_filepath, _options)
272 const filepaths = getConfigFileNames(resolvedFilePath, processedOptions)
273 const files = readConfigFilesSync(filepaths)
274 return parseFromConfigs(
275 getConfigsForFiles(files),
276 resolvedFilePath,
277 processedOptions,
278 )
279}