// Copyright (c) 2012, Compiler Explorer Authors // All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are met: // // * Redistributions of source code must retain the above copyright notice, // this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above copyright // notice, this list of conditions and the following disclaimer in the // documentation and/or other materials provided with the distribution. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE // POSSIBILITY OF SUCH DAMAGE. import fs from 'fs'; import path from 'path'; import _ from 'underscore'; import {isString} from '../shared/common-utils.js'; import type {LanguageKey} from '../types/languages.interfaces.js'; import {logger} from './logger.js'; import type {PropertyGetter, PropertyValue, Widen} from './properties.interfaces.js'; import {toProperty} from './utils.js'; let properties: Record> = {}; let hierarchy: string[] = []; let propDebug = false; function findProps(base: string, elem: string): Record { return properties[`${base}.${elem}`]; } function debug(string) { if (propDebug) logger.info(`prop: ${string}`); } export function get(base: string, property: string, defaultValue: undefined): PropertyValue; export function get( base: string, property: string, defaultValue: Widen, ): typeof defaultValue; export function get(base: string, property: string, defaultValue?: unknown): T; export function get(base: string, property: string, defaultValue?: unknown): unknown { let result = defaultValue; let source = 'default'; for (const elem of hierarchy) { const propertyMap = findProps(base, elem); if (propertyMap && property in propertyMap) { debug(`${base}.${property}: overriding ${source} value (${result}) with ${propertyMap[property]}`); result = propertyMap[property]; source = elem; } } debug(`${base}.${property}: returning ${result} (from ${source})`); return result; } export type RawPropertiesGetter = typeof get; export function parseProperties(blob: string, name) { const props: Record = {}; for (const [index, lineOrig] of blob.split('\n').entries()) { const line = lineOrig.replace(/#.*/, '').trim(); if (!line) continue; const split = line.match(/([^=]+)=(.*)/); if (!split) { logger.error(`Bad line: ${line} in ${name}: ${index + 1}`); continue; } const prop = split[1].trim(); let val: string | number | boolean = split[2].trim(); // hack to avoid applying toProperty to version properties // so that they're not parsed as numbers if (!prop.endsWith('.version') && !prop.endsWith('.semver')) { val = toProperty(val); } props[prop] = val; debug(`${prop} = ${val}`); } return props; } export function initialize(directory: string, hier) { if (hier === null) throw new Error('Must supply a hierarchy array'); hierarchy = hier.map(x => x.toLowerCase()); logger.info(`Reading properties from ${directory} with hierarchy ${hierarchy}`); const endsWith = /\.properties$/; const propertyFiles = fs.readdirSync(directory).filter(filename => filename.match(endsWith)); properties = {}; for (let file of propertyFiles) { const baseName = file.replace(endsWith, ''); file = path.join(directory, file); debug('Reading config from ' + file); properties[baseName] = parseProperties(fs.readFileSync(file, 'utf8'), file); } logger.debug('props.properties = ', properties); } export function reset() { hierarchy = []; properties = {}; logger.debug('Properties reset'); } export function propsFor(base): PropertyGetter { return function (property, defaultValue) { return get(base, property, defaultValue); }; } // function mappedOf(fn, funcA, funcB) { // const resultA = funcA(); // if (resultA !== undefined) return resultA; // return funcB(); // } export type LanguageDef = { id: string; }; /*** * Compiler property fetcher */ export class CompilerProps { public readonly languages: Record; public readonly propsByLangId: Record; public readonly ceProps: PropertyGetter; /*** * Creates a CompilerProps lookup function */ constructor(languages: Record, ceProps: PropertyGetter) { this.languages = languages; this.propsByLangId = {}; this.ceProps = ceProps; // Instantiate a function to access records concerning the chosen language in hidden object props.properties _.each(this.languages, lang => (this.propsByLangId[lang.id] = propsFor(lang.id))); } $getInternal(base: string, property: string, defaultValue: undefined): PropertyValue; $getInternal(base: string, property: string, defaultValue: Widen): typeof defaultValue; $getInternal(base: string, property: string, defaultValue?: PropertyValue): T; $getInternal(langId: string, key: string, defaultValue: PropertyValue): PropertyValue { const languagePropertyValue = this.propsByLangId[langId](key); if (languagePropertyValue !== undefined) { return languagePropertyValue; } return this.ceProps(key, defaultValue); } /*** * Gets a value for a given key associated to the given languages from the properties * * @param langs - Which langs to search in * For compatibility, {null} means looking into the Compiler Explorer properties (Not on any language) * If langs is a {string}, it refers to the language id we want to search into * If langs is a {CELanguages}, it refers to which languages we want to search into * TODO: Add a {Language} version? * @param {string} key - Key to look for * @param {*} defaultValue - What to return if the key is not found * @param {?function} fn - Transformation to give to each value found * @returns {*} Transformed value(s) found or fn(defaultValue) */ // A lot of overloads for a lot of different variants: // const a = this.compilerProps(lang, property); // PropertyValue // const b = this.compilerProps(lang, property); // number // const c = this.compilerProps(lang, property, 42); // number // const d = this.compilerProps(lang, property, undefined, (x) => ["foobar"]); // string[] // const e = this.compilerProps(lang, property, 42, (x) => ["foobar"]); // number | string[] // if more than one language: // const f = this.compilerProps(languages, property); // Record // const g = this.compilerProps(languages, property); // Record // const h = this.compilerProps(languages, property, 42); // Record // const i = this.compilerProps(languages, property, undefined, (x) => ["foobar"]); // Record // const j = this.compilerProps(languages, property, 42, (x) => ["foobar"]);//Record // TODO(jeremy-rifkin): I think the types could use some work here. // Maybe this.compilerProps(lang, property) should be number | undefined. // general overloads get(base: string, property: string, defaultValue?: undefined, fn?: undefined): PropertyValue; get( base: string, property: string, defaultValue: Widen, fn?: undefined, ): typeof defaultValue; get(base: string, property: string, defaultValue?: PropertyValue, fn?: undefined): T; // fn overloads get( base: string, property: string, defaultValue?: undefined, fn?: (item: PropertyValue, language?: any) => R, ): R; get( base: string, property: string, defaultValue: Widen, fn?: (item: typeof defaultValue, language?: any) => R, ): R; // container base general overloads get( base: LanguageDef[] | Record, property: string, defaultValue?: undefined, fn?: undefined, ): Record; get( base: LanguageDef[] | Record, property: string, defaultValue: Widen, fn?: undefined, ): Record; get( base: LanguageDef[] | Record, property: string, defaultValue?: PropertyValue, fn?: undefined, ): Record; // container base fn overloads get( base: LanguageDef[] | Record, property: string, defaultValue?: undefined, fn?: (item: PropertyValue, language?: any) => R, ): Record; get( base: LanguageDef[] | Record, property: string, defaultValue: Widen, fn?: (item: typeof defaultValue, language?: any) => R, ): Record; get( langs: string | LanguageDef[] | Record, key: string, defaultValue?: PropertyValue, fn?: (item: PropertyValue, language?: any) => unknown, ) { const map_fn = fn || _.identity; if (_.isEmpty(langs)) { return map_fn(this.ceProps(key, defaultValue)); } if (isString(langs)) { if (this.propsByLangId[langs]) { return map_fn(this.$getInternal(langs, key, defaultValue), this.languages[langs]); } else { logger.error(`Tried to pass ${langs} as a language ID`); return map_fn(defaultValue); } } else { return _.chain(langs) .map(lang => [lang.id, map_fn(this.$getInternal(lang.id, key, defaultValue), lang)]) .object() .value(); } } } export function setDebug(debug: boolean) { propDebug = debug; } export function fakeProps(fake: Record): PropertyGetter { return (prop, def) => (fake[prop] === undefined ? def : fake[prop]); } export function getRawProperties() { return properties; }