UNPKG

@eagleoutice/flowr

Version:

Static Dataflow Analyzer and Program Slicer for the R Programming Language

205 lines 9.53 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NAMING_CONVENTION = exports.CasingConvention = void 0; exports.detectPotentialCasings = detectPotentialCasings; exports.detectCasing = detectCasing; exports.getMostUsedCasing = getMostUsedCasing; exports.fixCasing = fixCasing; exports.createNamingConventionQuickFixes = createNamingConventionQuickFixes; const vertex_1 = require("../../dataflow/graph/vertex"); const dfg_get_symbol_refs_1 = require("../../dataflow/origin/dfg-get-symbol-refs"); const flowr_search_builder_1 = require("../../search/flowr-search-builder"); const assert_1 = require("../../util/assert"); const range_1 = require("../../util/range"); const linter_format_1 = require("../linter-format"); const linter_tags_1 = require("../linter-tags"); var CasingConvention; (function (CasingConvention) { CasingConvention["CamelCase"] = "camelCase"; CasingConvention["PascalCase"] = "PascalCase"; CasingConvention["SnakeCase"] = "snake_case"; CasingConvention["ConstantCase"] = "CONSTANT_CASE"; CasingConvention["CamelSnakeCase"] = "camel_Snake_Case"; CasingConvention["PascalSnakeCase"] = "Pascal_Snake_Case"; CasingConvention["Unknown"] = "unknown"; })(CasingConvention || (exports.CasingConvention = CasingConvention = {})); function containsAlpha(s) { return /[A-Za-z]/.test(s); } /** * Attempts to detect the possible casing conventions used in the given identifier and returns an array ordered by likelihood of the casing convention being correct. */ function detectPotentialCasings(identifier, ignorePrefix) { if (identifier.trim() === '' || !containsAlpha(identifier)) { return []; } if (ignorePrefix) { identifier = identifier.replace(new RegExp(`^(${ignorePrefix})`), ''); } const upper = identifier.toUpperCase(); const lower = identifier.toLowerCase(); const isAllUpper = identifier === upper; const isAllLower = identifier === lower; const hasUnderscores = identifier.includes('_'); const upperAfterAllScores = Array(identifier.length - 1).keys().every(i => identifier[i] !== '_' || identifier[i + 1] === upper[i + 1]); const hasAnyUpperAfterLower = Array(identifier.length - 1).keys().some(i => containsAlpha(identifier[i]) && identifier[i] === lower[i] && containsAlpha(identifier[i + 1]) && identifier[i + 1] === upper[i + 1]); const matches = []; if (!hasUnderscores && identifier[0] === lower[0]) { matches.push(CasingConvention.CamelCase); // camelCase } if (!hasUnderscores && identifier[0] === upper[0] && (identifier.length === 1 || !isAllUpper)) { matches.push(CasingConvention.PascalCase); // PascalCase or Pascalcase } if (isAllUpper) { matches.push(CasingConvention.ConstantCase); // CONSTANT_CASE or CONSTANTCASE } if (isAllLower) { matches.push(CasingConvention.SnakeCase); // snake_case or snakecase or snakecase_ } if (upperAfterAllScores && identifier[0] === lower[0] && !isAllUpper && hasUnderscores || (!hasUnderscores && isAllLower)) { matches.push(CasingConvention.CamelSnakeCase); // camel_Snake_Case or camelsnakecase or camelsnakecase_ } if (upperAfterAllScores && identifier[0] === upper[0] && (identifier.length === 1 || !isAllUpper) && !hasAnyUpperAfterLower) { matches.push(CasingConvention.PascalSnakeCase); // Pascal_Snake_Case or Pascalsnakecase } return matches; } /** * Attempts to detect the possible casing conventions used in the given identifier and returns the first result. * The function {@link detectPotentialCasings} is generally preferred, as it returns all potential casings and not just the first one. */ function detectCasing(identifier, ignorePrefix) { const casings = detectPotentialCasings(identifier, ignorePrefix); return casings.length > 0 ? casings[0] : CasingConvention.Unknown; } /** * Determines the most used casing convention in the given list of symbols. */ function getMostUsedCasing(symbols) { if (symbols.length === 0) { return CasingConvention.Unknown; } const map = new Map(); for (const symbol of symbols) { const o = map.get(symbol.detectedCasing) ?? 0; map.set(symbol.detectedCasing, o + 1); } // Return element with most occurances return [...map].reduce((p, c) => p[1] > c[1] ? p : c)[0]; } /** * Attempts to fix the casing of the given identifier to match the provided convention. */ function fixCasing(identifier, convention, ignorePrefix) { if (!containsAlpha(identifier)) { return undefined; } if (ignorePrefix) { identifier = identifier.replace(new RegExp(`^(${ignorePrefix})`), ''); } const splitOn = identifier.includes('_') ? /_/ : /(?=[A-Z])/; const tokens = identifier.split(splitOn).map(s => s.toLowerCase()); const firstUp = (s) => { if (s.length < 1) { return s.toUpperCase(); } return `${s[0].toUpperCase()}${s.substring(1)}`; }; switch (convention) { case CasingConvention.CamelCase: // camelCase return `${tokens[0]}${tokens.slice(1).map(firstUp).join('')}`; case CasingConvention.PascalCase: // PascalCase return tokens.map(firstUp).join(''); case CasingConvention.SnakeCase: // snake_case return tokens.join('_'); case CasingConvention.ConstantCase: // CONSTANT_CASE return tokens.map(s => s.toUpperCase()).join('_'); case CasingConvention.CamelSnakeCase: // camel_Snake_Case return `${tokens[0]}_${tokens.slice(1).map(firstUp).join('_')}`; case CasingConvention.PascalSnakeCase: // Pascal_Snake_Case return tokens.map(firstUp).join('_'); case CasingConvention.Unknown: return identifier; default: (0, assert_1.assertUnreachable)(convention); } } /** * Creates quick fixes for renaming all references to the given node to match the provided replacement. */ function createNamingConventionQuickFixes(graph, nodeId, replacement, conv) { const refs = (0, dfg_get_symbol_refs_1.getAllRefsToSymbol)(graph, nodeId); const idMap = graph.idMap; if (refs === undefined || idMap === undefined) { return undefined; } const result = []; for (const ref of refs) { const node = idMap.get(ref); if (node === undefined) { continue; } const loc = range_1.SourceLocation.fromNode(node); if (loc) { // In case of a function call we only need to include the name, not the '()' loc[3] = loc[1] + node.lexeme.length - 1; result.push({ type: 'replace', replacement: replacement, description: `Rename to match naming convention ${conv}`, loc: loc }); } } return result.length === 0 ? undefined : // We sort so that when applied in order the fixes will start from the end of the line to avoid conflicts result.sort((a, b) => range_1.SourceLocation.compare(b.loc, a.loc)); } exports.NAMING_CONVENTION = { createSearch: (_config) => flowr_search_builder_1.Q.all().filter(vertex_1.VertexType.VariableDefinition), processSearchResult: (elements, config, data) => { const symbols = elements.getElements() .map(m => ({ certainty: linter_format_1.LintingResultCertainty.Certain, detectedCasing: detectCasing(m.node.lexeme, config.ignorePrefix), name: m.node.lexeme, loc: range_1.SourceLocation.fromNode(m.node), id: m.node.info.id })).filter(e => (0, assert_1.isNotUndefined)(e.loc)); const casing = config.caseing === 'auto' ? getMostUsedCasing(symbols) : config.caseing; const results = symbols .filter(m => (m.detectedCasing !== casing) && (!config.ignoreNonAlpha || containsAlpha(m.name))) .map(({ id, ...m }) => { const fix = fixCasing(m.name, casing, config.ignorePrefix); return { ...m, involvedId: id, quickFix: fix ? createNamingConventionQuickFixes(data.dataflow.graph, id, fix, casing) : undefined }; }); return { results: results, '.meta': { numMatches: symbols.length - results.length, numBreak: results.length } }; }, prettyPrint: { [linter_format_1.LintingPrettyPrintContext.Query]: result => `Identifier '${result.name}' at ${range_1.SourceLocation.format(result.loc)} (${result.detectedCasing})`, [linter_format_1.LintingPrettyPrintContext.Full]: result => `Identifier '${result.name}' at ${range_1.SourceLocation.format(result.loc)} follows wrong convention: ${result.detectedCasing}` }, info: { name: 'Naming Convention', // detects casing heuristically so correctness is not ensured using default config, but checks all identifiers in the code for naming convention match certainty: linter_format_1.LintingRuleCertainty.OverApproximative, description: 'Checks whether the symbols conform to a certain naming convention', tags: [linter_tags_1.LintingRuleTag.Style, linter_tags_1.LintingRuleTag.QuickFix], defaultConfig: { caseing: 'auto', ignoreNonAlpha: true } } }; //# sourceMappingURL=naming-convention.js.map