@eagleoutice/flowr
Version:
Static Dataflow Analyzer and Program Slicer for the R Programming Language
205 lines • 9.53 kB
JavaScript
;
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