Website Structure

This commit is contained in:
supalerk-ar66 2026-01-13 10:46:40 +07:00
parent 62812f2090
commit 71f0676a62
22365 changed files with 4265753 additions and 791 deletions

View file

@ -0,0 +1,2 @@
import type { RuleModule } from "./types";
export declare const rules: RuleModule[];

View file

@ -0,0 +1,172 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.rules = void 0;
const confusing_quantifier_1 = __importDefault(require("./rules/confusing-quantifier"));
const control_character_escape_1 = __importDefault(require("./rules/control-character-escape"));
const grapheme_string_literal_1 = __importDefault(require("./rules/grapheme-string-literal"));
const hexadecimal_escape_1 = __importDefault(require("./rules/hexadecimal-escape"));
const letter_case_1 = __importDefault(require("./rules/letter-case"));
const match_any_1 = __importDefault(require("./rules/match-any"));
const negation_1 = __importDefault(require("./rules/negation"));
const no_contradiction_with_assertion_1 = __importDefault(require("./rules/no-contradiction-with-assertion"));
const no_control_character_1 = __importDefault(require("./rules/no-control-character"));
const no_dupe_characters_character_class_1 = __importDefault(require("./rules/no-dupe-characters-character-class"));
const no_dupe_disjunctions_1 = __importDefault(require("./rules/no-dupe-disjunctions"));
const no_empty_alternative_1 = __importDefault(require("./rules/no-empty-alternative"));
const no_empty_capturing_group_1 = __importDefault(require("./rules/no-empty-capturing-group"));
const no_empty_character_class_1 = __importDefault(require("./rules/no-empty-character-class"));
const no_empty_group_1 = __importDefault(require("./rules/no-empty-group"));
const no_empty_lookarounds_assertion_1 = __importDefault(require("./rules/no-empty-lookarounds-assertion"));
const no_empty_string_literal_1 = __importDefault(require("./rules/no-empty-string-literal"));
const no_escape_backspace_1 = __importDefault(require("./rules/no-escape-backspace"));
const no_extra_lookaround_assertions_1 = __importDefault(require("./rules/no-extra-lookaround-assertions"));
const no_invalid_regexp_1 = __importDefault(require("./rules/no-invalid-regexp"));
const no_invisible_character_1 = __importDefault(require("./rules/no-invisible-character"));
const no_lazy_ends_1 = __importDefault(require("./rules/no-lazy-ends"));
const no_legacy_features_1 = __importDefault(require("./rules/no-legacy-features"));
const no_misleading_capturing_group_1 = __importDefault(require("./rules/no-misleading-capturing-group"));
const no_misleading_unicode_character_1 = __importDefault(require("./rules/no-misleading-unicode-character"));
const no_missing_g_flag_1 = __importDefault(require("./rules/no-missing-g-flag"));
const no_non_standard_flag_1 = __importDefault(require("./rules/no-non-standard-flag"));
const no_obscure_range_1 = __importDefault(require("./rules/no-obscure-range"));
const no_octal_1 = __importDefault(require("./rules/no-octal"));
const no_optional_assertion_1 = __importDefault(require("./rules/no-optional-assertion"));
const no_potentially_useless_backreference_1 = __importDefault(require("./rules/no-potentially-useless-backreference"));
const no_standalone_backslash_1 = __importDefault(require("./rules/no-standalone-backslash"));
const no_super_linear_backtracking_1 = __importDefault(require("./rules/no-super-linear-backtracking"));
const no_super_linear_move_1 = __importDefault(require("./rules/no-super-linear-move"));
const no_trivially_nested_assertion_1 = __importDefault(require("./rules/no-trivially-nested-assertion"));
const no_trivially_nested_quantifier_1 = __importDefault(require("./rules/no-trivially-nested-quantifier"));
const no_unused_capturing_group_1 = __importDefault(require("./rules/no-unused-capturing-group"));
const no_useless_assertions_1 = __importDefault(require("./rules/no-useless-assertions"));
const no_useless_backreference_1 = __importDefault(require("./rules/no-useless-backreference"));
const no_useless_character_class_1 = __importDefault(require("./rules/no-useless-character-class"));
const no_useless_dollar_replacements_1 = __importDefault(require("./rules/no-useless-dollar-replacements"));
const no_useless_escape_1 = __importDefault(require("./rules/no-useless-escape"));
const no_useless_flag_1 = __importDefault(require("./rules/no-useless-flag"));
const no_useless_lazy_1 = __importDefault(require("./rules/no-useless-lazy"));
const no_useless_non_capturing_group_1 = __importDefault(require("./rules/no-useless-non-capturing-group"));
const no_useless_quantifier_1 = __importDefault(require("./rules/no-useless-quantifier"));
const no_useless_range_1 = __importDefault(require("./rules/no-useless-range"));
const no_useless_set_operand_1 = __importDefault(require("./rules/no-useless-set-operand"));
const no_useless_string_literal_1 = __importDefault(require("./rules/no-useless-string-literal"));
const no_useless_two_nums_quantifier_1 = __importDefault(require("./rules/no-useless-two-nums-quantifier"));
const no_zero_quantifier_1 = __importDefault(require("./rules/no-zero-quantifier"));
const optimal_lookaround_quantifier_1 = __importDefault(require("./rules/optimal-lookaround-quantifier"));
const optimal_quantifier_concatenation_1 = __importDefault(require("./rules/optimal-quantifier-concatenation"));
const prefer_character_class_1 = __importDefault(require("./rules/prefer-character-class"));
const prefer_d_1 = __importDefault(require("./rules/prefer-d"));
const prefer_escape_replacement_dollar_char_1 = __importDefault(require("./rules/prefer-escape-replacement-dollar-char"));
const prefer_lookaround_1 = __importDefault(require("./rules/prefer-lookaround"));
const prefer_named_backreference_1 = __importDefault(require("./rules/prefer-named-backreference"));
const prefer_named_capture_group_1 = __importDefault(require("./rules/prefer-named-capture-group"));
const prefer_named_replacement_1 = __importDefault(require("./rules/prefer-named-replacement"));
const prefer_plus_quantifier_1 = __importDefault(require("./rules/prefer-plus-quantifier"));
const prefer_predefined_assertion_1 = __importDefault(require("./rules/prefer-predefined-assertion"));
const prefer_quantifier_1 = __importDefault(require("./rules/prefer-quantifier"));
const prefer_question_quantifier_1 = __importDefault(require("./rules/prefer-question-quantifier"));
const prefer_range_1 = __importDefault(require("./rules/prefer-range"));
const prefer_regexp_exec_1 = __importDefault(require("./rules/prefer-regexp-exec"));
const prefer_regexp_test_1 = __importDefault(require("./rules/prefer-regexp-test"));
const prefer_result_array_groups_1 = __importDefault(require("./rules/prefer-result-array-groups"));
const prefer_set_operation_1 = __importDefault(require("./rules/prefer-set-operation"));
const prefer_star_quantifier_1 = __importDefault(require("./rules/prefer-star-quantifier"));
const prefer_unicode_codepoint_escapes_1 = __importDefault(require("./rules/prefer-unicode-codepoint-escapes"));
const prefer_w_1 = __importDefault(require("./rules/prefer-w"));
const require_unicode_regexp_1 = __importDefault(require("./rules/require-unicode-regexp"));
const require_unicode_sets_regexp_1 = __importDefault(require("./rules/require-unicode-sets-regexp"));
const simplify_set_operations_1 = __importDefault(require("./rules/simplify-set-operations"));
const sort_alternatives_1 = __importDefault(require("./rules/sort-alternatives"));
const sort_character_class_elements_1 = __importDefault(require("./rules/sort-character-class-elements"));
const sort_flags_1 = __importDefault(require("./rules/sort-flags"));
const strict_1 = __importDefault(require("./rules/strict"));
const unicode_escape_1 = __importDefault(require("./rules/unicode-escape"));
const unicode_property_1 = __importDefault(require("./rules/unicode-property"));
const use_ignore_case_1 = __importDefault(require("./rules/use-ignore-case"));
exports.rules = [
confusing_quantifier_1.default,
control_character_escape_1.default,
grapheme_string_literal_1.default,
hexadecimal_escape_1.default,
letter_case_1.default,
match_any_1.default,
negation_1.default,
no_contradiction_with_assertion_1.default,
no_control_character_1.default,
no_dupe_characters_character_class_1.default,
no_dupe_disjunctions_1.default,
no_empty_alternative_1.default,
no_empty_capturing_group_1.default,
no_empty_character_class_1.default,
no_empty_group_1.default,
no_empty_lookarounds_assertion_1.default,
no_empty_string_literal_1.default,
no_escape_backspace_1.default,
no_extra_lookaround_assertions_1.default,
no_invalid_regexp_1.default,
no_invisible_character_1.default,
no_lazy_ends_1.default,
no_legacy_features_1.default,
no_misleading_capturing_group_1.default,
no_misleading_unicode_character_1.default,
no_missing_g_flag_1.default,
no_non_standard_flag_1.default,
no_obscure_range_1.default,
no_octal_1.default,
no_optional_assertion_1.default,
no_potentially_useless_backreference_1.default,
no_standalone_backslash_1.default,
no_super_linear_backtracking_1.default,
no_super_linear_move_1.default,
no_trivially_nested_assertion_1.default,
no_trivially_nested_quantifier_1.default,
no_unused_capturing_group_1.default,
no_useless_assertions_1.default,
no_useless_backreference_1.default,
no_useless_character_class_1.default,
no_useless_dollar_replacements_1.default,
no_useless_escape_1.default,
no_useless_flag_1.default,
no_useless_lazy_1.default,
no_useless_non_capturing_group_1.default,
no_useless_quantifier_1.default,
no_useless_range_1.default,
no_useless_set_operand_1.default,
no_useless_string_literal_1.default,
no_useless_two_nums_quantifier_1.default,
no_zero_quantifier_1.default,
optimal_lookaround_quantifier_1.default,
optimal_quantifier_concatenation_1.default,
prefer_character_class_1.default,
prefer_d_1.default,
prefer_escape_replacement_dollar_char_1.default,
prefer_lookaround_1.default,
prefer_named_backreference_1.default,
prefer_named_capture_group_1.default,
prefer_named_replacement_1.default,
prefer_plus_quantifier_1.default,
prefer_predefined_assertion_1.default,
prefer_quantifier_1.default,
prefer_question_quantifier_1.default,
prefer_range_1.default,
prefer_regexp_exec_1.default,
prefer_regexp_test_1.default,
prefer_result_array_groups_1.default,
prefer_set_operation_1.default,
prefer_star_quantifier_1.default,
prefer_unicode_codepoint_escapes_1.default,
prefer_w_1.default,
require_unicode_regexp_1.default,
require_unicode_sets_regexp_1.default,
simplify_set_operations_1.default,
sort_alternatives_1.default,
sort_character_class_elements_1.default,
sort_flags_1.default,
strict_1.default,
unicode_escape_1.default,
unicode_property_1.default,
use_ignore_case_1.default,
];

View file

@ -0,0 +1,2 @@
export { rules } from "./rules/all";
export declare const plugins: string[];

View file

@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.plugins = exports.rules = void 0;
var all_1 = require("./rules/all");
Object.defineProperty(exports, "rules", { enumerable: true, get: function () { return all_1.rules; } });
exports.plugins = ["regexp"];

View file

@ -0,0 +1,5 @@
import * as plugin from "../../index";
export { rules } from "../rules/all";
export declare const plugins: {
regexp: typeof plugin;
};

View file

@ -0,0 +1,40 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.plugins = exports.rules = void 0;
const plugin = __importStar(require("../../index"));
var all_1 = require("../rules/all");
Object.defineProperty(exports, "rules", { enumerable: true, get: function () { return all_1.rules; } });
exports.plugins = { regexp: plugin };

View file

@ -0,0 +1,5 @@
import * as plugin from "../../index";
export { rules } from "../rules/recommended";
export declare const plugins: {
regexp: typeof plugin;
};

View file

@ -0,0 +1,40 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.plugins = exports.rules = void 0;
const plugin = __importStar(require("../../index"));
var recommended_1 = require("../rules/recommended");
Object.defineProperty(exports, "rules", { enumerable: true, get: function () { return recommended_1.rules; } });
exports.plugins = { regexp: plugin };

View file

@ -0,0 +1,2 @@
export { rules } from "./rules/recommended";
export declare const plugins: string[];

View file

@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.plugins = exports.rules = void 0;
var recommended_1 = require("./rules/recommended");
Object.defineProperty(exports, "rules", { enumerable: true, get: function () { return recommended_1.rules; } });
exports.plugins = ["regexp"];

View file

@ -0,0 +1,4 @@
import type { SeverityString } from "../../types";
export declare const rules: {
[x: string]: SeverityString;
};

View file

@ -0,0 +1,13 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rules = void 0;
const all_rules_1 = require("../../all-rules");
const recommended_1 = require("./recommended");
const all = {};
for (const rule of all_rules_1.rules) {
all[rule.meta.docs.ruleId] = "error";
}
exports.rules = {
...all,
...recommended_1.rules,
};

View file

@ -0,0 +1,2 @@
import type { SeverityString } from "../../types";
export declare const rules: Record<string, SeverityString>;

View file

@ -0,0 +1,72 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.rules = void 0;
exports.rules = {
"no-control-regex": "error",
"no-misleading-character-class": "error",
"no-regex-spaces": "error",
"prefer-regex-literals": "error",
"no-invalid-regexp": "off",
"no-useless-backreference": "off",
"no-empty-character-class": "off",
"regexp/confusing-quantifier": "warn",
"regexp/control-character-escape": "error",
"regexp/match-any": "error",
"regexp/negation": "error",
"regexp/no-contradiction-with-assertion": "error",
"regexp/no-dupe-characters-character-class": "error",
"regexp/no-dupe-disjunctions": "error",
"regexp/no-empty-alternative": "warn",
"regexp/no-empty-capturing-group": "error",
"regexp/no-empty-character-class": "error",
"regexp/no-empty-group": "error",
"regexp/no-empty-lookarounds-assertion": "error",
"regexp/no-empty-string-literal": "error",
"regexp/no-escape-backspace": "error",
"regexp/no-extra-lookaround-assertions": "error",
"regexp/no-invalid-regexp": "error",
"regexp/no-invisible-character": "error",
"regexp/no-lazy-ends": "warn",
"regexp/no-legacy-features": "error",
"regexp/no-misleading-capturing-group": "error",
"regexp/no-misleading-unicode-character": "error",
"regexp/no-missing-g-flag": "error",
"regexp/no-non-standard-flag": "error",
"regexp/no-obscure-range": "error",
"regexp/no-optional-assertion": "error",
"regexp/no-potentially-useless-backreference": "warn",
"regexp/no-super-linear-backtracking": "error",
"regexp/no-trivially-nested-assertion": "error",
"regexp/no-trivially-nested-quantifier": "error",
"regexp/no-unused-capturing-group": "error",
"regexp/no-useless-assertions": "error",
"regexp/no-useless-backreference": "error",
"regexp/no-useless-character-class": "error",
"regexp/no-useless-dollar-replacements": "error",
"regexp/no-useless-escape": "error",
"regexp/no-useless-flag": "warn",
"regexp/no-useless-lazy": "error",
"regexp/no-useless-non-capturing-group": "error",
"regexp/no-useless-quantifier": "error",
"regexp/no-useless-range": "error",
"regexp/no-useless-set-operand": "error",
"regexp/no-useless-string-literal": "error",
"regexp/no-useless-two-nums-quantifier": "error",
"regexp/no-zero-quantifier": "error",
"regexp/optimal-lookaround-quantifier": "warn",
"regexp/optimal-quantifier-concatenation": "error",
"regexp/prefer-character-class": "error",
"regexp/prefer-d": "error",
"regexp/prefer-plus-quantifier": "error",
"regexp/prefer-predefined-assertion": "error",
"regexp/prefer-question-quantifier": "error",
"regexp/prefer-range": "error",
"regexp/prefer-set-operation": "error",
"regexp/prefer-star-quantifier": "error",
"regexp/prefer-unicode-codepoint-escapes": "error",
"regexp/prefer-w": "error",
"regexp/simplify-set-operations": "error",
"regexp/sort-flags": "error",
"regexp/strict": "error",
"regexp/use-ignore-case": "error",
};

View file

@ -0,0 +1,15 @@
import * as all from "./configs/all";
import * as flatAll from "./configs/flat/all";
import * as flatRecommended from "./configs/flat/recommended";
import * as recommended from "./configs/recommended";
import type { RuleModule } from "./types";
export * as meta from "./meta";
export declare const configs: {
recommended: typeof recommended;
all: typeof all;
"flat/all": typeof flatAll;
"flat/recommended": typeof flatRecommended;
};
export declare const rules: {
[key: string]: RuleModule;
};

View file

@ -0,0 +1,52 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.rules = exports.configs = exports.meta = void 0;
const all_rules_1 = require("./all-rules");
const all = __importStar(require("./configs/all"));
const flatAll = __importStar(require("./configs/flat/all"));
const flatRecommended = __importStar(require("./configs/flat/recommended"));
const recommended = __importStar(require("./configs/recommended"));
exports.meta = __importStar(require("./meta"));
exports.configs = {
recommended,
all,
"flat/all": flatAll,
"flat/recommended": flatRecommended,
};
exports.rules = all_rules_1.rules.reduce((obj, r) => {
obj[r.meta.docs.ruleName] = r;
return obj;
}, {});

View file

@ -0,0 +1 @@
export declare const name: any, version: any;

View file

@ -0,0 +1,5 @@
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.version = exports.name = void 0;
_a = require("../package.json"), exports.name = _a.name, exports.version = _a.version;

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const regexp_ast_1 = require("../utils/regexp-ast");
exports.default = (0, utils_1.createRule)("confusing-quantifier", {
meta: {
docs: {
description: "disallow confusing quantifiers",
category: "Best Practices",
recommended: true,
default: "warn",
},
schema: [],
messages: {
confusing: "This quantifier is confusing because its minimum is {{min}} but it can match the empty string. Maybe replace it with `{{proposal}}` to reflect that it can match the empty string?",
},
type: "problem",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
return {
onQuantifierEnter(qNode) {
if (qNode.min > 0 &&
(0, regexp_ast_analysis_1.isPotentiallyEmpty)(qNode.element, flags)) {
const proposal = (0, regexp_ast_1.quantToString)({ ...qNode, min: 0 });
context.report({
node,
loc: getRegexpLocation(qNode, (0, regexp_ast_1.getQuantifierOffsets)(qNode)),
messageId: "confusing",
data: {
min: String(qNode.min),
proposal,
},
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,74 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const utils_2 = require("../utils/ast-utils/utils");
const mention_1 = require("../utils/mention");
const CONTROL_CHARS = new Map([
[0, "\\0"],
[utils_1.CP_TAB, "\\t"],
[utils_1.CP_LF, "\\n"],
[utils_1.CP_VT, "\\v"],
[utils_1.CP_FF, "\\f"],
[utils_1.CP_CR, "\\r"],
]);
function isRegExpLiteralAt({ node, patternSource }, at) {
if ((0, utils_2.isRegexpLiteral)(node)) {
return true;
}
const replaceRange = patternSource.getReplaceRange(at);
if (replaceRange && replaceRange.type === "RegExp") {
return true;
}
return false;
}
exports.default = (0, utils_1.createRule)("control-character-escape", {
meta: {
docs: {
description: "enforce consistent escaping of control characters",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
unexpected: "Unexpected control character escape {{actual}}. Use '{{expected}}' instead.",
},
type: "suggestion",
},
create(context) {
function createVisitor(regexpContext) {
const { node, getRegexpLocation, fixReplaceNode } = regexpContext;
return {
onCharacterEnter(cNode) {
if (cNode.parent.type === "CharacterClassRange") {
return;
}
const expectedRaw = CONTROL_CHARS.get(cNode.value);
if (expectedRaw === undefined) {
return;
}
if (cNode.raw === expectedRaw) {
return;
}
if (!isRegExpLiteralAt(regexpContext, cNode) &&
cNode.raw === String.fromCodePoint(cNode.value)) {
return;
}
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
data: {
actual: (0, mention_1.mentionChar)(cNode),
expected: expectedRaw,
},
fix: fixReplaceNode(cNode, expectedRaw),
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,53 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const segmenter = new Intl.Segmenter();
exports.default = (0, utils_1.createRule)("grapheme-string-literal", {
meta: {
docs: {
description: "enforce single grapheme in string literal",
category: "Stylistic Issues",
recommended: false,
},
schema: [],
messages: {
onlySingleCharacters: "Only single characters and graphemes are allowed inside character classes. Use regular alternatives (e.g. `{{alternatives}}`) for strings instead.",
},
type: "suggestion",
},
create(context) {
function createVisitor(regexpContext) {
const { node, getRegexpLocation } = regexpContext;
function isMultipleGraphemes(saNode) {
if (saNode.elements.length <= 1)
return false;
const string = String.fromCodePoint(...saNode.elements.map((element) => element.value));
const segments = [...segmenter.segment(string)];
return segments.length > 1;
}
function buildAlternativeExample(saNode) {
const alternativeRaws = saNode.parent.alternatives
.filter(isMultipleGraphemes)
.map((alt) => alt.raw);
return `(?:${alternativeRaws.join("|")}|[...])`;
}
return {
onStringAlternativeEnter(saNode) {
if (!isMultipleGraphemes(saNode))
return;
context.report({
node,
loc: getRegexpLocation(saNode),
messageId: "onlySingleCharacters",
data: {
alternatives: buildAlternativeExample(saNode),
},
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,77 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const regex_syntax_1 = require("../utils/regex-syntax");
exports.default = (0, utils_1.createRule)("hexadecimal-escape", {
meta: {
docs: {
description: "enforce consistent usage of hexadecimal escape",
category: "Stylistic Issues",
recommended: false,
},
fixable: "code",
schema: [
{
enum: ["always", "never"],
},
],
messages: {
expectedHexEscape: "Expected hexadecimal escape ('{{hexEscape}}'), but {{unexpectedKind}} escape ('{{rejectEscape}}') is used.",
unexpectedHexEscape: "Unexpected hexadecimal escape ('{{hexEscape}}').",
},
type: "suggestion",
},
create(context) {
const always = context.options[0] !== "never";
function verifyForAlways({ node, getRegexpLocation, fixReplaceNode }, kind, cNode) {
if (kind !== regex_syntax_1.EscapeSequenceKind.unicode &&
kind !== regex_syntax_1.EscapeSequenceKind.unicodeCodePoint) {
return;
}
const hexEscape = `\\x${cNode.value.toString(16).padStart(2, "0")}`;
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "expectedHexEscape",
data: {
hexEscape,
unexpectedKind: kind,
rejectEscape: cNode.raw,
},
fix: fixReplaceNode(cNode, hexEscape),
});
}
function verifyForNever({ node, getRegexpLocation, fixReplaceNode }, kind, cNode) {
if (kind !== regex_syntax_1.EscapeSequenceKind.hexadecimal) {
return;
}
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpectedHexEscape",
data: {
hexEscape: cNode.raw,
},
fix: fixReplaceNode(cNode, () => `\\u00${cNode.raw.slice(2)}`),
});
}
const verify = always ? verifyForAlways : verifyForNever;
function createVisitor(regexpContext) {
return {
onCharacterEnter(cNode) {
if (cNode.value > 0xff) {
return;
}
const kind = (0, regex_syntax_1.getEscapeSequenceKind)(cNode.raw);
if (!kind) {
return;
}
verify(regexpContext, kind, cNode);
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,159 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const regex_syntax_1 = require("../utils/regex-syntax");
const CASE_SCHEMA = ["lowercase", "uppercase", "ignore"];
const DEFAULTS = {
caseInsensitive: "lowercase",
unicodeEscape: "lowercase",
hexadecimalEscape: "lowercase",
controlEscape: "uppercase",
};
function parseOptions(option) {
if (!option) {
return DEFAULTS;
}
return {
caseInsensitive: option.caseInsensitive || DEFAULTS.caseInsensitive,
unicodeEscape: option.unicodeEscape || DEFAULTS.unicodeEscape,
hexadecimalEscape: option.hexadecimalEscape || DEFAULTS.hexadecimalEscape,
controlEscape: option.controlEscape || DEFAULTS.controlEscape,
};
}
const CODE_POINT_CASE_CHECKER = {
lowercase: utils_1.isLowercaseLetter,
uppercase: utils_1.isUppercaseLetter,
};
const STRING_CASE_CHECKER = {
lowercase: (s) => s.toLowerCase() === s,
uppercase: (s) => s.toUpperCase() === s,
};
const CONVERTER = {
lowercase: (s) => s.toLowerCase(),
uppercase: (s) => s.toUpperCase(),
};
exports.default = (0, utils_1.createRule)("letter-case", {
meta: {
docs: {
description: "enforce into your favorite case",
category: "Stylistic Issues",
recommended: false,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
caseInsensitive: { enum: CASE_SCHEMA },
unicodeEscape: { enum: CASE_SCHEMA },
hexadecimalEscape: { enum: CASE_SCHEMA },
controlEscape: { enum: CASE_SCHEMA },
},
additionalProperties: false,
},
],
messages: {
unexpected: "'{{char}}' is not in {{case}}",
},
type: "layout",
},
create(context) {
const options = parseOptions(context.options[0]);
function report({ node, getRegexpLocation, fixReplaceNode }, reportNode, letterCase, convertText) {
context.report({
node,
loc: getRegexpLocation(reportNode),
messageId: "unexpected",
data: {
char: reportNode.raw,
case: letterCase,
},
fix: fixReplaceNode(reportNode, () => convertText(CONVERTER[letterCase])),
});
}
function verifyCharacterInCaseInsensitive(regexpContext, cNode) {
if (cNode.parent.type === "CharacterClassRange" ||
options.caseInsensitive === "ignore") {
return;
}
if (CODE_POINT_CASE_CHECKER[options.caseInsensitive](cNode.value) ||
!(0, utils_1.isLetter)(cNode.value)) {
return;
}
report(regexpContext, cNode, options.caseInsensitive, (converter) => converter(String.fromCodePoint(cNode.value)));
}
function verifyCharacterClassRangeInCaseInsensitive(regexpContext, ccrNode) {
if (options.caseInsensitive === "ignore") {
return;
}
if (CODE_POINT_CASE_CHECKER[options.caseInsensitive](ccrNode.min.value) ||
!(0, utils_1.isLetter)(ccrNode.min.value) ||
CODE_POINT_CASE_CHECKER[options.caseInsensitive](ccrNode.max.value) ||
!(0, utils_1.isLetter)(ccrNode.max.value)) {
return;
}
report(regexpContext, ccrNode, options.caseInsensitive, (converter) => `${converter(String.fromCodePoint(ccrNode.min.value))}-${converter(String.fromCodePoint(ccrNode.max.value))}`);
}
function verifyCharacterInUnicodeEscape(regexpContext, cNode) {
if (options.unicodeEscape === "ignore") {
return;
}
const parts = /^(?<prefix>\\u\{?)(?<code>.*?)(?<suffix>\}?)$/u.exec(cNode.raw);
if (STRING_CASE_CHECKER[options.unicodeEscape](parts.groups.code)) {
return;
}
report(regexpContext, cNode, options.unicodeEscape, (converter) => `${parts.groups.prefix}${converter(parts.groups.code)}${parts.groups.suffix}`);
}
function verifyCharacterInHexadecimalEscape(regexpContext, cNode) {
if (options.hexadecimalEscape === "ignore") {
return;
}
const parts = /^\\x(?<code>.*)$/u.exec(cNode.raw);
if (STRING_CASE_CHECKER[options.hexadecimalEscape](parts.groups.code)) {
return;
}
report(regexpContext, cNode, options.hexadecimalEscape, (converter) => `\\x${converter(parts.groups.code)}`);
}
function verifyCharacterInControl(regexpContext, cNode) {
if (options.controlEscape === "ignore") {
return;
}
const parts = /^\\c(?<code>.*)$/u.exec(cNode.raw);
if (STRING_CASE_CHECKER[options.controlEscape](parts.groups.code)) {
return;
}
report(regexpContext, cNode, options.controlEscape, (converter) => `\\c${converter(parts.groups.code)}`);
}
function createVisitor(regexpContext) {
const { flags } = regexpContext;
return {
onCharacterEnter(cNode) {
if (flags.ignoreCase) {
verifyCharacterInCaseInsensitive(regexpContext, cNode);
}
const escapeKind = (0, regex_syntax_1.getEscapeSequenceKind)(cNode.raw);
if (escapeKind === regex_syntax_1.EscapeSequenceKind.unicode ||
escapeKind === regex_syntax_1.EscapeSequenceKind.unicodeCodePoint) {
verifyCharacterInUnicodeEscape(regexpContext, cNode);
}
if (escapeKind === regex_syntax_1.EscapeSequenceKind.hexadecimal) {
verifyCharacterInHexadecimalEscape(regexpContext, cNode);
}
if (escapeKind === regex_syntax_1.EscapeSequenceKind.control) {
verifyCharacterInControl(regexpContext, cNode);
}
},
...(flags.ignoreCase
? {
onCharacterClassRangeEnter(ccrNode) {
verifyCharacterClassRangeInCaseInsensitive(regexpContext, ccrNode);
},
}
: {}),
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,134 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const utils_2 = require("../utils/ast-utils/utils");
const mention_1 = require("../utils/mention");
const OPTION_SS1 = "[\\s\\S]";
const OPTION_SS2 = "[\\S\\s]";
const OPTION_CARET = "[^]";
const OPTION_DOTALL = "dotAll";
exports.default = (0, utils_1.createRule)("match-any", {
meta: {
docs: {
description: "enforce match any character style",
category: "Stylistic Issues",
recommended: true,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
allows: {
type: "array",
items: {
type: "string",
enum: [
OPTION_SS1,
OPTION_SS2,
OPTION_CARET,
OPTION_DOTALL,
],
},
uniqueItems: true,
minItems: 1,
},
},
additionalProperties: false,
},
],
messages: {
unexpected: "Unexpected using {{expr}} to match any character.",
},
type: "suggestion",
},
create(context) {
var _a, _b;
const sourceCode = context.sourceCode;
const allowList = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.allows) !== null && _b !== void 0 ? _b : [OPTION_SS1, OPTION_DOTALL];
const allows = new Set(allowList);
const preference = allowList[0] || null;
function fix(fixer, { node, flags, patternSource }, regexpNode) {
var _a, _b;
if (!preference) {
return null;
}
if (preference === OPTION_DOTALL) {
if (!flags.dotAll) {
return null;
}
if (!(0, utils_2.isRegexpLiteral)(node)) {
return null;
}
const range = patternSource.getReplaceRange(regexpNode);
if (range == null) {
return null;
}
const afterRange = [
range.range[1],
node.range[1],
];
return [
range.replace(fixer, "."),
fixer.replaceTextRange(afterRange, sourceCode.text.slice(...afterRange)),
];
}
if (regexpNode.type === "CharacterClass" &&
preference.startsWith("[") &&
preference.endsWith("]")) {
const range = patternSource.getReplaceRange({
start: regexpNode.start + 1,
end: regexpNode.end - 1,
});
return (_a = range === null || range === void 0 ? void 0 : range.replace(fixer, preference.slice(1, -1))) !== null && _a !== void 0 ? _a : null;
}
const range = patternSource.getReplaceRange(regexpNode);
return (_b = range === null || range === void 0 ? void 0 : range.replace(fixer, preference)) !== null && _b !== void 0 ? _b : null;
}
function createVisitor(regexpContext) {
const { node, flags, getRegexpLocation } = regexpContext;
function onClass(ccNode) {
if ((0, regexp_ast_analysis_1.matchesAllCharacters)(ccNode, flags) &&
!(0, regexp_ast_analysis_1.hasStrings)(ccNode, flags) &&
!allows.has(ccNode.raw)) {
context.report({
node,
loc: getRegexpLocation(ccNode),
messageId: "unexpected",
data: {
expr: (0, mention_1.mention)(ccNode),
},
fix(fixer) {
return fix(fixer, regexpContext, ccNode);
},
});
}
}
return {
onCharacterSetEnter(csNode) {
if (csNode.kind === "any" &&
flags.dotAll &&
!allows.has(OPTION_DOTALL)) {
context.report({
node,
loc: getRegexpLocation(csNode),
messageId: "unexpected",
data: {
expr: (0, mention_1.mention)(csNode),
},
fix(fixer) {
return fix(fixer, regexpContext, csNode);
},
});
}
},
onCharacterClassEnter: onClass,
onExpressionCharacterClassEnter: onClass,
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const util_1 = require("../utils/util");
function isNegatableCharacterClassElement(node) {
return (node.type === "CharacterClass" ||
node.type === "ExpressionCharacterClass" ||
(node.type === "CharacterSet" &&
(node.kind !== "property" || !node.strings)));
}
exports.default = (0, utils_1.createRule)("negation", {
meta: {
docs: {
description: "enforce use of escapes on negation",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
unexpected: "Unexpected negated character class. Use '{{negatedCharSet}}' instead.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, getRegexpLocation, fixReplaceNode, flags, }) {
return {
onCharacterClassEnter(ccNode) {
if (!ccNode.negate || ccNode.elements.length !== 1) {
return;
}
const element = ccNode.elements[0];
if (!isNegatableCharacterClassElement(element)) {
return;
}
if (element.type !== "CharacterSet" && !element.negate) {
return;
}
if (flags.ignoreCase &&
!flags.unicodeSets &&
element.type === "CharacterSet" &&
element.kind === "property") {
const ccSet = (0, regexp_ast_analysis_1.toUnicodeSet)(ccNode, flags);
const negatedElementSet = (0, regexp_ast_analysis_1.toUnicodeSet)({
...element,
negate: !element.negate,
}, flags);
if (!ccSet.equals(negatedElementSet)) {
return;
}
}
const negatedCharSet = getNegationText(element);
context.report({
node,
loc: getRegexpLocation(ccNode),
messageId: "unexpected",
data: { negatedCharSet },
fix: fixReplaceNode(ccNode, negatedCharSet),
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});
function getNegationText(node) {
if (node.type === "CharacterSet") {
let kind = node.raw[1];
if (kind.toLowerCase() === kind) {
kind = kind.toUpperCase();
}
else {
kind = kind.toLowerCase();
}
return `\\${kind}${node.raw.slice(2)}`;
}
if (node.type === "CharacterClass") {
return `[${node.elements.map((e) => e.raw).join("")}]`;
}
if (node.type === "ExpressionCharacterClass") {
return `[${node.raw.slice(2, -1)}]`;
}
return (0, util_1.assertNever)(node);
}

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,252 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const regexp_ast_1 = require("../utils/regexp-ast");
function isTrivialAssertion(assertion, dir, flags) {
if (assertion.kind !== "word") {
if ((0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind) !== dir) {
return true;
}
}
if (assertion.kind === "lookahead" || assertion.kind === "lookbehind") {
if ((0, regexp_ast_analysis_1.isPotentiallyEmpty)(assertion.alternatives, flags)) {
return true;
}
}
const look = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_analysis_1.getFirstConsumedChar)(assertion, dir, flags));
if (look.char.isEmpty || look.char.isAll) {
return true;
}
const after = (0, regexp_ast_analysis_1.getFirstCharAfter)(assertion, dir, flags);
if (!after.edge) {
if (look.exact && look.char.isSupersetOf(after.char)) {
return true;
}
if (look.char.isDisjointWith(after.char)) {
return true;
}
}
return false;
}
function* getNextElements(start, dir, flags) {
let element = start;
for (;;) {
const parent = element.parent;
if (parent.type === "CharacterClass" ||
parent.type === "CharacterClassRange" ||
parent.type === "ClassIntersection" ||
parent.type === "ClassSubtraction" ||
parent.type === "StringAlternative") {
return;
}
if (parent.type === "Quantifier") {
if (parent.max === 1) {
element = parent;
continue;
}
else {
return;
}
}
const elements = parent.elements;
const index = elements.indexOf(element);
const inc = dir === "ltr" ? 1 : -1;
for (let i = index + inc; i >= 0 && i < elements.length; i += inc) {
const e = elements[i];
yield e;
if (!(0, regexp_ast_analysis_1.isZeroLength)(e, flags)) {
return;
}
}
const grandParent = parent.parent;
if ((grandParent.type === "Group" ||
grandParent.type === "CapturingGroup" ||
(grandParent.type === "Assertion" &&
(0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(grandParent.kind) !==
dir)) &&
grandParent.alternatives.length === 1) {
element = grandParent;
continue;
}
return;
}
}
function tryFindContradictionIn(element, dir, condition, flags) {
if (condition(element)) {
return true;
}
if (element.type === "CapturingGroup" || element.type === "Group") {
let some = false;
element.alternatives.forEach((a) => {
if (tryFindContradictionInAlternative(a, dir, condition, flags)) {
some = true;
}
});
return some;
}
if (element.type === "Quantifier" && element.max === 1) {
return tryFindContradictionIn(element.element, dir, condition, flags);
}
if (element.type === "Assertion" &&
(element.kind === "lookahead" || element.kind === "lookbehind") &&
(0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(element.kind) === dir) {
element.alternatives.forEach((a) => tryFindContradictionInAlternative(a, dir, condition, flags));
}
return false;
}
function tryFindContradictionInAlternative(alternative, dir, condition, flags) {
if (condition(alternative)) {
return true;
}
const { elements } = alternative;
const first = dir === "ltr" ? 0 : elements.length;
const inc = dir === "ltr" ? 1 : -1;
for (let i = first; i >= 0 && i < elements.length; i += inc) {
const e = elements[i];
if (tryFindContradictionIn(e, dir, condition, flags)) {
return true;
}
if (!(0, regexp_ast_analysis_1.isZeroLength)(e, flags)) {
break;
}
}
return false;
}
function disjoint(a, b) {
if (a.edge && b.edge) {
return false;
}
return a.char.isDisjointWith(b.char);
}
exports.default = (0, utils_1.createRule)("no-contradiction-with-assertion", {
meta: {
docs: {
description: "disallow elements that contradict assertions",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
alternative: "The alternative {{ alt }} can never be entered because it contradicts with the assertion {{ assertion }}. Either change the alternative or assertion to resolve the contradiction.",
cannotEnterQuantifier: "The quantifier {{ quant }} can never be entered because its element contradicts with the assertion {{ assertion }}. Change or remove the quantifier or change the assertion to resolve the contradiction.",
alwaysEnterQuantifier: "The quantifier {{ quant }} is always entered despite having a minimum of 0. This is because the assertion {{ assertion }} contradicts with the element(s) after the quantifier. Either set the minimum to 1 ({{ newQuant }}) or change the assertion.",
removeQuantifier: "Remove the quantifier.",
changeQuantifier: "Change the quantifier to {{ newQuant }}.",
},
hasSuggestions: true,
type: "problem",
},
create(context) {
function createVisitor(regexpContext) {
const { node, flags, getRegexpLocation, fixReplaceQuant, fixReplaceNode, } = regexpContext;
function analyseAssertion(assertion, dir) {
if (isTrivialAssertion(assertion, dir, flags)) {
return;
}
const assertionLook = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_analysis_1.getFirstConsumedChar)(assertion, dir, flags));
for (const element of getNextElements(assertion, dir, flags)) {
if (tryFindContradictionIn(element, dir, contradicts, flags)) {
break;
}
}
function contradictsAlternative(alternative) {
let consumed = (0, regexp_ast_analysis_1.getFirstConsumedChar)(alternative, dir, flags);
if (consumed.empty) {
consumed = regexp_ast_analysis_1.FirstConsumedChars.concat([
consumed,
(0, regexp_ast_analysis_1.getFirstConsumedCharAfter)(alternative, dir, flags),
], flags);
}
const look = regexp_ast_analysis_1.FirstConsumedChars.toLook(consumed);
if (disjoint(assertionLook, look)) {
context.report({
node,
loc: getRegexpLocation(alternative),
messageId: "alternative",
data: {
assertion: (0, mention_1.mention)(assertion),
alt: (0, mention_1.mention)(alternative),
},
});
return true;
}
return false;
}
function contradictsQuantifier(quant) {
if (quant.max === 0) {
return false;
}
if (quant.min !== 0) {
return false;
}
const consumed = (0, regexp_ast_analysis_1.getFirstConsumedChar)(quant.element, dir, flags);
const look = regexp_ast_analysis_1.FirstConsumedChars.toLook(consumed);
if (disjoint(assertionLook, look)) {
context.report({
node,
loc: getRegexpLocation(quant),
messageId: "cannotEnterQuantifier",
data: {
assertion: (0, mention_1.mention)(assertion),
quant: (0, mention_1.mention)(quant),
},
suggest: [
{
messageId: "removeQuantifier",
fix: fixReplaceNode(quant, ""),
},
],
});
return true;
}
const after = (0, regexp_ast_analysis_1.getFirstCharAfter)(quant, dir, flags);
if (disjoint(assertionLook, after)) {
const newQuant = (0, regexp_ast_1.quantToString)({ ...quant, min: 1 });
context.report({
node,
loc: getRegexpLocation(quant),
messageId: "alwaysEnterQuantifier",
data: {
assertion: (0, mention_1.mention)(assertion),
quant: (0, mention_1.mention)(quant),
newQuant,
},
suggest: [
{
messageId: "changeQuantifier",
data: { newQuant },
fix: fixReplaceQuant(quant, {
min: 1,
max: quant.max,
}),
},
],
});
return true;
}
return false;
}
function contradicts(element) {
if (element.type === "Alternative") {
return contradictsAlternative(element);
}
else if (element.type === "Quantifier") {
return contradictsQuantifier(element);
}
return false;
}
}
return {
onAssertionEnter(assertion) {
analyseAssertion(assertion, "ltr");
analyseAssertion(assertion, "rtl");
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,84 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const unicode_1 = require("../utils/unicode");
const CONTROL_CHARS = new Map([
[0, "\\0"],
[unicode_1.CP_TAB, "\\t"],
[unicode_1.CP_LF, "\\n"],
[unicode_1.CP_VT, "\\v"],
[unicode_1.CP_FF, "\\f"],
[unicode_1.CP_CR, "\\r"],
]);
const ALLOWED_CONTROL_CHARS = /^\\[0fnrtv]$/u;
exports.default = (0, utils_1.createRule)("no-control-character", {
meta: {
docs: {
description: "disallow control characters",
category: "Possible Errors",
recommended: false,
},
schema: [],
messages: {
unexpected: "Unexpected control character {{ char }}.",
escape: "Use {{ escape }} instead.",
},
type: "suggestion",
hasSuggestions: true,
},
create(context) {
function createVisitor(regexpContext) {
const { node, patternSource, getRegexpLocation, fixReplaceNode } = regexpContext;
function isBadEscapeRaw(raw, cp) {
return (raw.codePointAt(0) === cp ||
raw.startsWith("\\x") ||
raw.startsWith("\\u"));
}
function isAllowedEscapeRaw(raw) {
return (ALLOWED_CONTROL_CHARS.test(raw) ||
(raw.startsWith("\\") &&
ALLOWED_CONTROL_CHARS.test(raw.slice(1))));
}
function isBadEscape(char) {
var _a;
const range = (_a = patternSource.getReplaceRange(char)) === null || _a === void 0 ? void 0 : _a.range;
const sourceRaw = range
? context.sourceCode.text.slice(...range)
: char.raw;
if (isAllowedEscapeRaw(char.raw) ||
isAllowedEscapeRaw(sourceRaw)) {
return false;
}
return (isBadEscapeRaw(char.raw, char.value) ||
(char.raw.startsWith("\\") &&
isBadEscapeRaw(char.raw.slice(1), char.value)));
}
return {
onCharacterEnter(cNode) {
if (cNode.value <= 0x1f && isBadEscape(cNode)) {
const suggest = [];
const allowedEscape = CONTROL_CHARS.get(cNode.value);
if (allowedEscape !== undefined) {
suggest.push({
messageId: "escape",
data: { escape: (0, mention_1.mention)(allowedEscape) },
fix: fixReplaceNode(cNode, allowedEscape),
});
}
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
data: { char: (0, mention_1.mentionChar)(cNode) },
suggest,
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,228 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const refa_1 = require("refa");
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const refa_2 = require("../utils/refa");
const util_1 = require("../utils/util");
function groupElements(elements, flags) {
const duplicates = [];
const characters = new Map();
const characterRanges = new Map();
const characterSetAndClasses = new Map();
function addToGroup(group, key, element) {
const current = group.get(key);
if (current !== undefined) {
duplicates.push({ element: current, duplicate: element });
}
else {
group.set(key, element);
}
}
for (const e of elements) {
if (e.type === "Character") {
const charSet = (0, regexp_ast_analysis_1.toCharSet)(e, flags);
const key = charSet.ranges[0].min;
addToGroup(characters, key, e);
}
else if (e.type === "CharacterClassRange") {
const charSet = (0, regexp_ast_analysis_1.toCharSet)(e, flags);
const key = buildRangeKey(charSet);
addToGroup(characterRanges, key, e);
}
else if (e.type === "CharacterSet" ||
e.type === "CharacterClass" ||
e.type === "ClassStringDisjunction" ||
e.type === "ExpressionCharacterClass") {
const key = e.raw;
addToGroup(characterSetAndClasses, key, e);
}
else {
(0, util_1.assertNever)(e);
}
}
return {
duplicates,
characters: [...characters.values()],
characterRanges: [...characterRanges.values()],
characterSetAndClasses: [...characterSetAndClasses.values()],
};
function buildRangeKey(rangeCharSet) {
return rangeCharSet.ranges
.map((r) => String.fromCodePoint(r.min, r.max))
.join(",");
}
}
function inRange({ min, max }, char) {
return min <= char && char <= max;
}
exports.default = (0, utils_1.createRule)("no-dupe-characters-character-class", {
meta: {
type: "suggestion",
docs: {
description: "disallow duplicate characters in the RegExp character class",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
duplicate: "Unexpected duplicate {{duplicate}}.",
duplicateNonObvious: "Unexpected duplicate. {{duplicate}} is a duplicate of {{element}}.",
subset: "{{subsetElement}} is already included in {{element}}.",
subsetOfMany: "{{subsetElement}} is already included by the elements {{elements}}.",
overlap: "Unexpected overlap of {{elementA}} and {{elementB}} was found '{{overlap}}'.",
},
},
create(context) {
function reportDuplicate(regexpContext, duplicate, element) {
const { node, getRegexpLocation } = regexpContext;
if (duplicate.raw === element.raw) {
context.report({
node,
loc: getRegexpLocation(duplicate),
messageId: "duplicate",
data: {
duplicate: (0, mention_1.mentionChar)(duplicate),
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, duplicate),
});
}
else {
context.report({
node,
loc: getRegexpLocation(duplicate),
messageId: "duplicateNonObvious",
data: {
duplicate: (0, mention_1.mentionChar)(duplicate),
element: (0, mention_1.mentionChar)(element),
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, duplicate),
});
}
}
function reportOverlap({ node, getRegexpLocation }, element, intersectElement, overlap) {
context.report({
node,
loc: getRegexpLocation(element),
messageId: "overlap",
data: {
elementA: (0, mention_1.mentionChar)(element),
elementB: (0, mention_1.mentionChar)(intersectElement),
overlap,
},
});
}
function reportSubset(regexpContext, subsetElement, element) {
const { node, getRegexpLocation } = regexpContext;
context.report({
node,
loc: getRegexpLocation(subsetElement),
messageId: "subset",
data: {
subsetElement: (0, mention_1.mentionChar)(subsetElement),
element: (0, mention_1.mentionChar)(element),
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, subsetElement),
});
}
function reportSubsetOfMany(regexpContext, subsetElement, elements) {
const { node, getRegexpLocation } = regexpContext;
context.report({
node,
loc: getRegexpLocation(subsetElement),
messageId: "subsetOfMany",
data: {
subsetElement: (0, mention_1.mentionChar)(subsetElement),
elements: `'${elements
.map((e) => e.raw)
.join("")}' (${elements.map(mention_1.mentionChar).join(", ")})`,
},
fix: (0, utils_1.fixRemoveCharacterClassElement)(regexpContext, subsetElement),
});
}
function createVisitor(regexpContext) {
const { flags } = regexpContext;
return {
onCharacterClassEnter(ccNode) {
const { duplicates, characters, characterRanges, characterSetAndClasses, } = groupElements(ccNode.elements, flags);
const elementsOtherThanCharacter = [
...characterRanges,
...characterSetAndClasses,
];
const subsets = new Set();
for (const { element, duplicate } of duplicates) {
reportDuplicate(regexpContext, duplicate, element);
subsets.add(duplicate);
}
for (const char of characters) {
for (const other of elementsOtherThanCharacter) {
if ((0, regexp_ast_analysis_1.toUnicodeSet)(other, flags).chars.has(char.value)) {
reportSubset(regexpContext, char, other);
subsets.add(char);
break;
}
}
}
for (const element of elementsOtherThanCharacter) {
for (const other of elementsOtherThanCharacter) {
if (element === other || subsets.has(other)) {
continue;
}
if ((0, regexp_ast_analysis_1.toUnicodeSet)(element, flags).isSubsetOf((0, regexp_ast_analysis_1.toUnicodeSet)(other, flags))) {
reportSubset(regexpContext, element, other);
subsets.add(element);
break;
}
}
}
const characterTotal = (0, regexp_ast_analysis_1.toUnicodeSet)(characters.filter((c) => !subsets.has(c)), flags);
for (const element of elementsOtherThanCharacter) {
if (subsets.has(element)) {
continue;
}
const totalOthers = characterTotal.union(...elementsOtherThanCharacter
.filter((e) => !subsets.has(e) && e !== element)
.map((e) => (0, regexp_ast_analysis_1.toUnicodeSet)(e, flags)));
const elementCharSet = (0, regexp_ast_analysis_1.toUnicodeSet)(element, flags);
if (elementCharSet.isSubsetOf(totalOthers)) {
const superSetElements = ccNode.elements
.filter((e) => !subsets.has(e) && e !== element)
.filter((e) => !(0, regexp_ast_analysis_1.toUnicodeSet)(e, flags).isDisjointWith(elementCharSet));
reportSubsetOfMany(regexpContext, element, superSetElements);
subsets.add(element);
}
}
for (let i = 0; i < characterRanges.length; i++) {
const range = characterRanges[i];
if (subsets.has(range)) {
continue;
}
for (let j = i + 1; j < elementsOtherThanCharacter.length; j++) {
const other = elementsOtherThanCharacter[j];
if (range === other || subsets.has(other)) {
continue;
}
const intersection = (0, regexp_ast_analysis_1.toUnicodeSet)(range, flags).intersect((0, regexp_ast_analysis_1.toUnicodeSet)(other, flags));
if (intersection.isEmpty) {
continue;
}
const interestingRanges = intersection.chars.ranges.filter((r) => inRange(r, range.min.value) ||
inRange(r, range.max.value));
(0, refa_2.assertValidFlags)(flags);
const interest = refa_1.JS.createCharSet(interestingRanges, flags);
if (!interest.isEmpty) {
reportOverlap(regexpContext, range, other, (0, refa_2.toCharSetSource)(interest, flags));
break;
}
}
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,863 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const refa_1 = require("refa");
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const char_ranges_1 = require("../utils/char-ranges");
const get_usage_of_pattern_1 = require("../utils/get-usage-of-pattern");
const mention_1 = require("../utils/mention");
const partial_parser_1 = require("../utils/partial-parser");
const refa_2 = require("../utils/refa");
const regexp_ast_1 = require("../utils/regexp-ast");
const util_1 = require("../utils/util");
function isStared(node) {
let max = (0, regexp_ast_analysis_1.getEffectiveMaximumRepetition)(node);
if (node.type === "Quantifier") {
max *= node.max;
}
return max > 10;
}
function hasNothingAfterNode(node) {
const md = (0, regexp_ast_analysis_1.getMatchingDirection)(node);
for (let p = node;; p = p.parent) {
if (p.type === "Assertion" || p.type === "Pattern") {
return true;
}
if (p.type !== "Alternative") {
const parent = p.parent;
if (parent.type === "Quantifier") {
if (parent.max > 1) {
return false;
}
}
else {
const lastIndex = md === "ltr" ? parent.elements.length - 1 : 0;
if (parent.elements[lastIndex] !== p) {
return false;
}
}
}
}
}
function containsAssertions(expression) {
try {
(0, refa_1.visitAst)(expression, {
onAssertionEnter() {
throw new Error();
},
});
return false;
}
catch (_a) {
return true;
}
}
function containsAssertionsOrUnknowns(expression) {
try {
(0, refa_1.visitAst)(expression, {
onAssertionEnter() {
throw new Error();
},
onUnknownEnter() {
throw new Error();
},
});
return false;
}
catch (_a) {
return true;
}
}
function isNonRegular(node) {
return (0, regexp_ast_analysis_1.hasSomeDescendant)(node, (d) => d.type === "Assertion" || d.type === "Backreference");
}
function toNFA(parser, element) {
try {
const { expression, maxCharacter } = parser.parseElement(element, {
backreferences: "unknown",
assertions: "parse",
});
let e;
if (containsAssertions(expression)) {
e = (0, refa_1.transform)(refa_1.Transformers.simplify({
ignoreAmbiguity: true,
ignoreOrder: true,
}), expression);
}
else {
e = expression;
}
return {
nfa: refa_1.NFA.fromRegex(e, { maxCharacter }, { assertions: "disable", unknowns: "disable" }),
partial: containsAssertionsOrUnknowns(e),
};
}
catch (_a) {
return {
nfa: refa_1.NFA.empty({
maxCharacter: parser.maxCharacter,
}),
partial: true,
};
}
}
function* iterateNestedAlternatives(alternative) {
for (const e of alternative.elements) {
if (e.type === "Group" || e.type === "CapturingGroup") {
for (const a of e.alternatives) {
if (e.alternatives.length > 1) {
yield a;
}
yield* iterateNestedAlternatives(a);
}
}
if (e.type === "CharacterClass" && !e.negate) {
const nested = [];
const addToNested = (charElement) => {
switch (charElement.type) {
case "CharacterClassRange": {
const min = charElement.min;
const max = charElement.max;
if (min.value === max.value) {
nested.push(charElement);
}
else if (min.value + 1 === max.value) {
nested.push(min, max);
}
else {
nested.push(charElement, min, max);
}
break;
}
case "ClassStringDisjunction": {
nested.push(...charElement.alternatives);
break;
}
case "CharacterClass": {
if (!charElement.negate) {
charElement.elements.forEach(addToNested);
}
else {
nested.push(charElement);
}
break;
}
case "Character":
case "CharacterSet":
case "ExpressionCharacterClass": {
nested.push(charElement);
break;
}
default:
throw (0, util_1.assertNever)(charElement);
}
};
e.elements.forEach(addToNested);
if (nested.length > 1)
yield* nested;
}
}
}
function* iteratePartialAlternatives(alternative, parser) {
if (isNonRegular(alternative)) {
return;
}
const maxCharacter = parser.maxCharacter;
const partialParser = new partial_parser_1.PartialParser(parser, {
assertions: "throw",
backreferences: "throw",
});
for (const nested of iterateNestedAlternatives(alternative)) {
try {
const expression = partialParser.parse(alternative, nested);
const nfa = refa_1.NFA.fromRegex(expression, { maxCharacter });
yield { nested, nfa };
}
catch (_a) {
}
}
}
function unionAll(nfas) {
if (nfas.length === 0) {
throw new Error("Cannot union 0 NFAs.");
}
else if (nfas.length === 1) {
return nfas[0];
}
const total = nfas[0].copy();
for (let i = 1; i < nfas.length; i++) {
total.union(nfas[i]);
}
return total;
}
const MAX_DFA_NODES = 100000;
function isSubsetOf(superset, subset) {
try {
const a = refa_1.DFA.fromIntersection(superset, subset, new refa_1.DFA.LimitedNodeFactory(MAX_DFA_NODES));
const b = refa_1.DFA.fromFA(subset, new refa_1.DFA.LimitedNodeFactory(MAX_DFA_NODES));
a.minimize();
b.minimize();
return a.structurallyEqual(b);
}
catch (_a) {
return null;
}
}
function getSubsetRelation(left, right) {
try {
const inter = refa_1.DFA.fromIntersection(left, right, new refa_1.DFA.LimitedNodeFactory(MAX_DFA_NODES));
inter.minimize();
const l = refa_1.DFA.fromFA(left, new refa_1.DFA.LimitedNodeFactory(MAX_DFA_NODES));
l.minimize();
const r = refa_1.DFA.fromFA(right, new refa_1.DFA.LimitedNodeFactory(MAX_DFA_NODES));
r.minimize();
const subset = l.structurallyEqual(inter);
const superset = r.structurallyEqual(inter);
if (subset && superset) {
return 1;
}
else if (subset) {
return 2;
}
else if (superset) {
return 3;
}
return 0;
}
catch (_a) {
return 4;
}
}
function getPartialSubsetRelation(left, leftIsPartial, right, rightIsPartial) {
const relation = getSubsetRelation(left, right);
if (!leftIsPartial && !rightIsPartial) {
return relation;
}
if (relation === 0 ||
relation === 4) {
return relation;
}
if (leftIsPartial && !rightIsPartial) {
switch (relation) {
case 1:
return 3;
case 2:
return 0;
case 3:
return 3;
default:
return (0, util_1.assertNever)(relation);
}
}
if (rightIsPartial && !leftIsPartial) {
switch (relation) {
case 1:
return 2;
case 2:
return 2;
case 3:
return 0;
default:
return (0, util_1.assertNever)(relation);
}
}
return 0;
}
function faToSource(fa, flags) {
try {
(0, refa_2.assertValidFlags)(flags);
return refa_1.JS.toLiteral(fa.toRegex(), { flags }).source;
}
catch (_a) {
return "<ERROR>";
}
}
function* findDuplicationAstFast(alternatives, flags) {
const shortCircuit = (a) => {
return a.type === "CapturingGroup" ? false : null;
};
for (let i = 0; i < alternatives.length; i++) {
const alternative = alternatives[i];
for (let j = 0; j < i; j++) {
const other = alternatives[j];
if ((0, regexp_ast_1.isEqualNodes)(other, alternative, flags, shortCircuit)) {
yield { type: "Duplicate", alternative, others: [other] };
}
}
}
}
function* findDuplicationAst(alternatives, flags, hasNothingAfter) {
const isCoveredOptions = {
flags,
canOmitRight: hasNothingAfter,
};
const isCoveredOptionsNoPrefix = {
flags,
canOmitRight: false,
};
for (let i = 0; i < alternatives.length; i++) {
const alternative = alternatives[i];
for (let j = 0; j < i; j++) {
const other = alternatives[j];
if ((0, regexp_ast_1.isCoveredNode)(other, alternative, isCoveredOptions)) {
if ((0, regexp_ast_1.isEqualNodes)(other, alternative, flags)) {
yield {
type: "Duplicate",
alternative,
others: [other],
};
}
else if (hasNothingAfter &&
!(0, regexp_ast_1.isCoveredNode)(other, alternative, isCoveredOptionsNoPrefix)) {
yield {
type: "PrefixSubset",
alternative,
others: [other],
};
}
else {
yield { type: "Subset", alternative, others: [other] };
}
}
}
}
}
function* findPrefixDuplicationNfa(alternatives, parser) {
if (alternatives.length === 0) {
return;
}
const all = refa_1.NFA.all({ maxCharacter: alternatives[0][0].maxCharacter });
for (let i = 0; i < alternatives.length; i++) {
const [nfa, partial, alternative] = alternatives[i];
if (!partial) {
const overlapping = alternatives
.slice(0, i)
.filter(([otherNfa]) => !(0, refa_1.isDisjointWith)(nfa, otherNfa));
if (overlapping.length >= 1) {
const othersNfa = unionAll(overlapping.map(([n]) => n));
const others = overlapping.map(([, , a]) => a);
if (isSubsetOf(othersNfa, nfa)) {
yield { type: "PrefixSubset", alternative, others };
}
else {
const nested = tryFindNestedSubsetResult(overlapping.map((o) => [o[0], o[2]]), othersNfa, alternative, parser);
if (nested) {
yield { ...nested, type: "PrefixNestedSubset" };
}
}
}
}
nfa.append(all);
}
}
function* findDuplicationNfa(alternatives, flags, { hasNothingAfter, parser, ignoreOverlap }) {
const previous = [];
for (let i = 0; i < alternatives.length; i++) {
const alternative = alternatives[i];
const { nfa, partial } = toNFA(parser, alternative);
const overlapping = previous.filter(([otherNfa]) => !(0, refa_1.isDisjointWith)(nfa, otherNfa));
if (overlapping.length >= 1) {
const othersNfa = unionAll(overlapping.map(([n]) => n));
const othersPartial = overlapping.some(([, p]) => p);
const others = overlapping.map(([, , a]) => a);
const relation = getPartialSubsetRelation(nfa, partial, othersNfa, othersPartial);
switch (relation) {
case 1:
if (others.length === 1) {
yield {
type: "Duplicate",
alternative,
others: [others[0]],
};
}
else {
yield { type: "Subset", alternative, others };
}
break;
case 2:
yield { type: "Subset", alternative, others };
break;
case 3: {
const reorder = (0, regexp_ast_analysis_1.canReorder)([alternative, ...others], flags);
if (reorder) {
for (const other of others) {
yield {
type: "Subset",
alternative: other,
others: [alternative],
};
}
}
else {
yield { type: "Superset", alternative, others };
}
break;
}
case 0:
case 4: {
const nested = tryFindNestedSubsetResult(overlapping.map((o) => [o[0], o[2]]), othersNfa, alternative, parser);
if (nested) {
yield nested;
break;
}
if (!ignoreOverlap) {
yield {
type: "Overlap",
alternative,
others,
overlap: refa_1.NFA.fromIntersection(nfa, othersNfa),
};
}
break;
}
default:
throw (0, util_1.assertNever)(relation);
}
}
previous.push([nfa, partial, alternative]);
}
if (hasNothingAfter) {
yield* findPrefixDuplicationNfa(previous, parser);
}
}
function tryFindNestedSubsetResult(others, othersNfa, alternative, parser) {
const disjointElements = new Set();
for (const { nested, nfa: nestedNfa } of iteratePartialAlternatives(alternative, parser)) {
if ((0, regexp_ast_analysis_1.hasSomeAncestor)(nested, (a) => disjointElements.has(a))) {
continue;
}
if ((0, refa_1.isDisjointWith)(othersNfa, nestedNfa)) {
disjointElements.add(nested);
continue;
}
if (isSubsetOf(othersNfa, nestedNfa)) {
return {
type: "NestedSubset",
alternative,
nested,
others: others
.filter((o) => !(0, refa_1.isDisjointWith)(o[0], nestedNfa))
.map((o) => o[1]),
};
}
}
return undefined;
}
function* findDuplication(alternatives, flags, options) {
if (options.fastAst) {
yield* findDuplicationAstFast(alternatives, flags);
}
else {
yield* findDuplicationAst(alternatives, flags, options.hasNothingAfter);
}
if (!options.noNfa) {
yield* findDuplicationNfa(alternatives, flags, options);
}
}
const RESULT_TYPE_ORDER = [
"Duplicate",
"Subset",
"NestedSubset",
"PrefixSubset",
"PrefixNestedSubset",
"Superset",
"Overlap",
];
function deduplicateResults(unsorted, { reportExp }) {
const results = [...unsorted].sort((a, b) => RESULT_TYPE_ORDER.indexOf(a.type) -
RESULT_TYPE_ORDER.indexOf(b.type));
const seen = new Map();
return results.filter(({ alternative, type }) => {
const firstSeen = seen.get(alternative);
if (firstSeen === undefined) {
seen.set(alternative, type);
return true;
}
if (reportExp &&
firstSeen === "PrefixSubset" &&
type !== "PrefixSubset") {
seen.set(alternative, type);
return true;
}
return false;
});
}
function mentionNested(nested) {
if (nested.type === "Alternative" || nested.type === "StringAlternative") {
return (0, mention_1.mention)(nested);
}
return (0, mention_1.mentionChar)(nested);
}
function fixRemoveNestedAlternative(context, alternative) {
switch (alternative.type) {
case "Alternative":
return (0, utils_1.fixRemoveAlternative)(context, alternative);
case "StringAlternative":
return (0, utils_1.fixRemoveStringAlternative)(context, alternative);
case "Character":
case "CharacterClassRange":
case "CharacterSet":
case "CharacterClass":
case "ExpressionCharacterClass":
case "ClassStringDisjunction": {
if (alternative.parent.type !== "CharacterClass") {
return () => null;
}
return (0, utils_1.fixRemoveCharacterClassElement)(context, alternative);
}
default:
throw (0, util_1.assertNever)(alternative);
}
}
exports.default = (0, utils_1.createRule)("no-dupe-disjunctions", {
meta: {
docs: {
description: "disallow duplicate disjunctions",
category: "Possible Errors",
recommended: true,
},
hasSuggestions: true,
schema: [
{
type: "object",
properties: {
report: {
type: "string",
enum: ["all", "trivial", "interesting"],
},
reportExponentialBacktracking: {
enum: ["none", "certain", "potential"],
},
reportUnreachable: {
enum: ["certain", "potential"],
},
},
additionalProperties: false,
},
],
messages: {
duplicate: "Unexpected duplicate alternative. This alternative can be removed.{{cap}}{{exp}}",
subset: "Unexpected useless alternative. This alternative is a strict subset of {{others}} and can be removed.{{cap}}{{exp}}",
nestedSubset: "Unexpected useless element. All paths of {{root}} that go through {{nested}} are a strict subset of {{others}}. This element can be removed.{{cap}}{{exp}}",
prefixSubset: "Unexpected useless alternative. This alternative is already covered by {{others}} and can be removed.{{cap}}",
prefixNestedSubset: "Unexpected useless element. All paths of {{root}} that go through {{nested}} are already covered by {{others}}. This element can be removed.{{cap}}",
superset: "Unexpected superset. This alternative is a superset of {{others}}. It might be possible to remove the other alternative(s).{{cap}}{{exp}}",
overlap: "Unexpected overlap. This alternative overlaps with {{others}}. The overlap is {{expr}}.{{cap}}{{exp}}",
remove: "Remove the {{alternative}} {{type}}.",
replaceRange: "Replace {{range}} with {{replacement}}.",
},
type: "suggestion",
},
create(context) {
var _a, _b, _c, _d, _e, _f;
const reportExponentialBacktracking = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.reportExponentialBacktracking) !== null && _b !== void 0 ? _b : "potential";
const reportUnreachable = (_d = (_c = context.options[0]) === null || _c === void 0 ? void 0 : _c.reportUnreachable) !== null && _d !== void 0 ? _d : "certain";
const report = (_f = (_e = context.options[0]) === null || _e === void 0 ? void 0 : _e.report) !== null && _f !== void 0 ? _f : "trivial";
const allowedRanges = (0, char_ranges_1.getAllowedCharRanges)(undefined, context);
function createVisitor(regexpContext) {
const { flags, node, getRegexpLocation, getUsageOfPattern } = regexpContext;
const parser = (0, refa_2.getParser)(regexpContext);
function getFilterInfo(parentNode) {
const usage = getUsageOfPattern();
let stared;
if (isStared(parentNode)) {
stared = 1;
}
else if (usage === get_usage_of_pattern_1.UsageOfPattern.partial ||
usage === get_usage_of_pattern_1.UsageOfPattern.mixed) {
stared = 2;
}
else {
stared = 0;
}
let nothingAfter;
if (!hasNothingAfterNode(parentNode)) {
nothingAfter = 0;
}
else if (usage === get_usage_of_pattern_1.UsageOfPattern.partial ||
usage === get_usage_of_pattern_1.UsageOfPattern.mixed) {
nothingAfter = 2;
}
else {
nothingAfter = 1;
}
let reportExp;
switch (reportExponentialBacktracking) {
case "none":
reportExp = false;
break;
case "certain":
reportExp = stared === 1;
break;
case "potential":
reportExp = stared !== 0;
break;
default:
(0, util_1.assertNever)(reportExponentialBacktracking);
}
let reportPrefix;
switch (reportUnreachable) {
case "certain":
reportPrefix = nothingAfter === 1;
break;
case "potential":
reportPrefix = nothingAfter !== 0;
break;
default:
(0, util_1.assertNever)(reportUnreachable);
}
return { stared, nothingAfter, reportExp, reportPrefix };
}
function verify(parentNode) {
const info = getFilterInfo(parentNode);
const rawResults = findDuplication(parentNode.alternatives, flags, {
fastAst: false,
noNfa: false,
ignoreOverlap: !info.reportExp && report !== "all",
hasNothingAfter: info.reportPrefix,
parser,
});
let results = filterResults([...rawResults], info);
results = deduplicateResults(results, info);
results.forEach((result) => reportResult(result, info));
}
function filterResults(results, { nothingAfter, reportExp, reportPrefix }) {
switch (report) {
case "all": {
return results;
}
case "trivial": {
return results.filter(({ type }) => {
switch (type) {
case "Duplicate":
case "Subset":
case "NestedSubset":
return true;
case "Overlap":
case "Superset":
return reportExp;
case "PrefixSubset":
case "PrefixNestedSubset":
return reportPrefix;
default:
throw (0, util_1.assertNever)(type);
}
});
}
case "interesting": {
return results.filter(({ type }) => {
switch (type) {
case "Duplicate":
case "Subset":
case "NestedSubset":
return true;
case "Overlap":
return reportExp;
case "Superset":
return (reportExp ||
nothingAfter === 0);
case "PrefixSubset":
case "PrefixNestedSubset":
return reportPrefix;
default:
throw (0, util_1.assertNever)(type);
}
});
}
default:
throw (0, util_1.assertNever)(report);
}
}
function printChar(char) {
if ((0, char_ranges_1.inRange)(allowedRanges, char)) {
return String.fromCodePoint(char);
}
if (char === 0)
return "\\0";
if (char <= 0xff)
return `\\x${char.toString(16).padStart(2, "0")}`;
if (char <= 0xffff)
return `\\u${char.toString(16).padStart(4, "0")}`;
return `\\u{${char.toString(16)}}`;
}
function getSuggestions(result) {
if (result.type === "Overlap" || result.type === "Superset") {
return [];
}
const alternative = result.type === "NestedSubset" ||
result.type === "PrefixNestedSubset"
? result.nested
: result.alternative;
const containsCapturingGroup = (0, regexp_ast_analysis_1.hasSomeDescendant)(alternative, (d) => d.type === "CapturingGroup");
if (containsCapturingGroup) {
return [];
}
if (alternative.type === "Character" &&
alternative.parent.type === "CharacterClassRange") {
const range = alternative.parent;
let replacement;
if (range.min.value + 1 === range.max.value) {
replacement =
range.min === alternative
? range.max.raw
: range.min.raw;
}
else {
if (range.min === alternative) {
const min = printChar(range.min.value + 1);
replacement = `${min}-${range.max.raw}`;
}
else {
const max = printChar(range.max.value - 1);
replacement = `${range.min.raw}-${max}`;
}
}
return [
{
messageId: "replaceRange",
data: {
range: (0, mention_1.mentionChar)(range),
replacement: (0, mention_1.mention)(replacement),
},
fix: regexpContext.fixReplaceNode(range, replacement),
},
];
}
return [
{
messageId: "remove",
data: {
alternative: mentionNested(alternative),
type: alternative.type === "Alternative"
? "alternative"
: "element",
},
fix: fixRemoveNestedAlternative(regexpContext, alternative),
},
];
}
function reportResult(result, { stared }) {
let exp;
if (stared === 1) {
exp =
" This ambiguity is likely to cause exponential backtracking.";
}
else if (stared === 2) {
exp =
" This ambiguity might cause exponential backtracking.";
}
else {
exp = "";
}
const reportAlternative = result.type === "NestedSubset" ||
result.type === "PrefixNestedSubset"
? result.nested
: result.alternative;
const loc = getRegexpLocation(reportAlternative);
const cap = (0, regexp_ast_analysis_1.hasSomeDescendant)(reportAlternative, (d) => d.type === "CapturingGroup")
? " Careful! This alternative contains capturing groups which might be difficult to remove."
: "";
const others = (0, mention_1.mention)(result.others.map((a) => a.raw).join("|"));
const suggest = getSuggestions(result);
switch (result.type) {
case "Duplicate":
context.report({
node,
loc,
messageId: "duplicate",
data: { exp, cap, others },
suggest,
});
break;
case "Subset":
context.report({
node,
loc,
messageId: "subset",
data: { exp, cap, others },
suggest,
});
break;
case "NestedSubset":
context.report({
node,
loc,
messageId: "nestedSubset",
data: {
exp,
cap,
others,
root: (0, mention_1.mention)(result.alternative),
nested: mentionNested(result.nested),
},
suggest,
});
break;
case "PrefixSubset":
context.report({
node,
loc,
messageId: "prefixSubset",
data: { exp, cap, others },
suggest,
});
break;
case "PrefixNestedSubset":
context.report({
node,
loc,
messageId: "prefixNestedSubset",
data: {
exp,
cap,
others,
root: (0, mention_1.mention)(result.alternative),
nested: mentionNested(result.nested),
},
suggest,
});
break;
case "Superset":
context.report({
node,
loc,
messageId: "superset",
data: { exp, cap, others },
suggest,
});
break;
case "Overlap":
context.report({
node,
loc,
messageId: "overlap",
data: {
exp,
cap,
others,
expr: (0, mention_1.mention)(faToSource(result.overlap, flags)),
},
suggest,
});
break;
default:
throw (0, util_1.assertNever)(result);
}
}
return {
onPatternEnter: verify,
onGroupEnter: verify,
onCapturingGroupEnter: verify,
onAssertionEnter(aNode) {
if (aNode.kind === "lookahead" ||
aNode.kind === "lookbehind") {
verify(aNode);
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,101 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
function getCapturingGroupOuterSource(node) {
const first = node.alternatives[0];
const last = node.alternatives[node.alternatives.length - 1];
const innerStart = first.start - node.start;
const innerEnd = last.end - node.start;
return [node.raw.slice(0, innerStart), node.raw.slice(innerEnd)];
}
function getFixedNode(regexpNode, alt) {
var _a;
let quant;
if (regexpNode.alternatives.at(0) === alt) {
quant = "??";
}
else if (regexpNode.alternatives.at(-1) === alt) {
quant = "?";
}
else {
return null;
}
const innerAlternatives = regexpNode.alternatives
.filter((a) => a !== alt)
.map((a) => a.raw)
.join("|");
let replacement = `(?:${innerAlternatives})${quant}`;
if (regexpNode.type === "CapturingGroup") {
const [before, after] = getCapturingGroupOuterSource(regexpNode);
replacement = `${before}${replacement}${after}`;
}
else if (((_a = regexpNode.parent) === null || _a === void 0 ? void 0 : _a.type) === "Quantifier") {
replacement = `(?:${replacement})`;
}
return replacement;
}
exports.default = (0, utils_1.createRule)("no-empty-alternative", {
meta: {
docs: {
description: "disallow alternatives without elements",
category: "Possible Errors",
recommended: true,
default: "warn",
},
schema: [],
hasSuggestions: true,
messages: {
empty: "This empty alternative might be a mistake. If not, use a quantifier instead.",
suggest: "Use a quantifier instead.",
},
type: "problem",
},
create(context) {
function createVisitor({ node, getRegexpLocation, fixReplaceNode, }) {
function verifyAlternatives(regexpNode, suggestFixer) {
if (regexpNode.alternatives.length >= 2) {
for (let i = 0; i < regexpNode.alternatives.length; i++) {
const alt = regexpNode.alternatives[i];
const isLast = i === regexpNode.alternatives.length - 1;
if (alt.elements.length === 0) {
const index = alt.start;
const loc = isLast
? getRegexpLocation({
start: index - 1,
end: index,
})
: getRegexpLocation({
start: index,
end: index + 1,
});
const fixed = suggestFixer(alt);
context.report({
node,
loc,
messageId: "empty",
suggest: fixed
? [
{
messageId: "suggest",
fix: fixReplaceNode(regexpNode, fixed),
},
]
: undefined,
});
return;
}
}
}
}
return {
onGroupEnter: (gNode) => verifyAlternatives(gNode, (alt) => getFixedNode(gNode, alt)),
onCapturingGroupEnter: (cgNode) => verifyAlternatives(cgNode, (alt) => getFixedNode(cgNode, alt)),
onPatternEnter: (pNode) => verifyAlternatives(pNode, (alt) => getFixedNode(pNode, alt)),
onClassStringDisjunctionEnter: (csdNode) => verifyAlternatives(csdNode, () => null),
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-empty-capturing-group", {
meta: {
docs: {
description: "disallow capturing group that captures empty.",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
unexpected: "Unexpected capture empty.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
return {
onCapturingGroupEnter(cgNode) {
if ((0, regexp_ast_analysis_1.isZeroLength)(cgNode, flags)) {
context.report({
node,
loc: getRegexpLocation(cgNode),
messageId: "unexpected",
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,49 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-empty-character-class", {
meta: {
docs: {
description: "disallow character classes that match no characters",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
empty: "This character class matches no characters because it is empty.",
cannotMatchAny: "This character class cannot match any characters.",
},
type: "suggestion",
},
create(context) {
function createVisitor(regexpContext) {
const { node, getRegexpLocation, flags } = regexpContext;
return {
onCharacterClassEnter(ccNode) {
if ((0, regexp_ast_analysis_1.matchesNoCharacters)(ccNode, flags)) {
context.report({
node,
loc: getRegexpLocation(ccNode),
messageId: ccNode.elements.length
? "cannotMatchAny"
: "empty",
});
}
},
onExpressionCharacterClassEnter(ccNode) {
if ((0, regexp_ast_analysis_1.matchesNoCharacters)(ccNode, flags)) {
context.report({
node,
loc: getRegexpLocation(ccNode),
messageId: "cannotMatchAny",
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-empty-group", {
meta: {
docs: {
description: "disallow empty group",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
unexpected: "Unexpected empty group.",
},
type: "suggestion",
},
create(context) {
function verifyGroup({ node, getRegexpLocation }, gNode) {
if (gNode.alternatives.every((alt) => alt.elements.length === 0)) {
context.report({
node,
loc: getRegexpLocation(gNode),
messageId: "unexpected",
});
}
}
function createVisitor(regexpContext) {
return {
onGroupEnter(gNode) {
verifyGroup(regexpContext, gNode);
},
onCapturingGroupEnter(cgNode) {
verifyGroup(regexpContext, cgNode);
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-empty-lookarounds-assertion", {
meta: {
docs: {
description: "disallow empty lookahead assertion or empty lookbehind assertion",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
unexpected: "Unexpected empty {{kind}}. It will trivially {{result}} all inputs.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
return {
onAssertionEnter(aNode) {
if (aNode.kind !== "lookahead" &&
aNode.kind !== "lookbehind") {
return;
}
if ((0, regexp_ast_analysis_1.isPotentiallyEmpty)(aNode.alternatives, flags)) {
context.report({
node,
loc: getRegexpLocation(aNode),
messageId: "unexpected",
data: {
kind: aNode.kind,
result: aNode.negate ? "reject" : "accept",
},
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-empty-string-literal", {
meta: {
docs: {
description: "disallow empty string literals in character classes",
category: "Best Practices",
recommended: true,
},
schema: [],
messages: {
unexpected: "Unexpected empty string literal.",
},
type: "suggestion",
},
create(context) {
function createVisitor(regexpContext) {
const { node, getRegexpLocation } = regexpContext;
return {
onClassStringDisjunctionEnter(csdNode) {
if (csdNode.alternatives.every((alt) => alt.elements.length === 0)) {
context.report({
node,
loc: getRegexpLocation(csdNode),
messageId: "unexpected",
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-escape-backspace", {
meta: {
docs: {
description: "disallow escape backspace (`[\\b]`)",
category: "Possible Errors",
recommended: true,
},
schema: [],
hasSuggestions: true,
messages: {
unexpected: "Unexpected '[\\b]'. Use '\\u0008' instead.",
suggest: "Use '\\u0008'.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, getRegexpLocation, fixReplaceNode, }) {
return {
onCharacterEnter(cNode) {
if (cNode.value === utils_1.CP_BACKSPACE && cNode.raw === "\\b") {
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
suggest: [
{
messageId: "suggest",
fix: fixReplaceNode(cNode, "\\u0008"),
},
],
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,70 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-extra-lookaround-assertions", {
meta: {
docs: {
description: "disallow unnecessary nested lookaround assertions",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
canBeInlined: "This {{kind}} assertion is useless and can be inlined.",
canBeConvertedIntoGroup: "This {{kind}} assertion is useless and can be converted into a group.",
},
type: "suggestion",
},
create(context) {
function createVisitor(regexpContext) {
return {
onAssertionEnter(aNode) {
if (aNode.kind === "lookahead" ||
aNode.kind === "lookbehind") {
verify(regexpContext, aNode);
}
},
};
}
function verify(regexpContext, assertion) {
for (const alternative of assertion.alternatives) {
const nested = alternative.elements.at(assertion.kind === "lookahead"
?
-1
:
0);
if ((nested === null || nested === void 0 ? void 0 : nested.type) === "Assertion" &&
nested.kind === assertion.kind &&
!nested.negate) {
reportLookaroundAssertion(regexpContext, nested);
}
}
}
function reportLookaroundAssertion({ node, getRegexpLocation, fixReplaceNode }, assertion) {
let messageId, replaceText;
if (assertion.alternatives.length === 1) {
messageId = "canBeInlined";
replaceText = assertion.alternatives[0].raw;
}
else {
messageId = "canBeConvertedIntoGroup";
replaceText = `(?:${assertion.alternatives
.map((alt) => alt.raw)
.join("|")})`;
}
context.report({
node,
loc: getRegexpLocation(assertion),
messageId,
data: {
kind: assertion.kind,
},
fix: fixReplaceNode(assertion, replaceText),
});
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,71 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
function getErrorIndex(error) {
const index = error.index;
if (typeof index === "number") {
return index;
}
return null;
}
exports.default = (0, utils_1.createRule)("no-invalid-regexp", {
meta: {
docs: {
description: "disallow invalid regular expression strings in `RegExp` constructors",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
error: "{{message}}",
duplicateFlag: "Duplicate {{flag}} flag.",
uvFlag: "Regex 'u' and 'v' flags cannot be used together.",
},
type: "problem",
},
create(context) {
function visitInvalid(regexpContext) {
const { node, error, patternSource } = regexpContext;
let loc = undefined;
const index = getErrorIndex(error);
if (index !== null &&
index >= 0 &&
index <= patternSource.value.length) {
loc = patternSource.getAstLocation({
start: Math.max(index - 1, 0),
end: Math.min(index + 1, patternSource.value.length),
});
}
context.report({
node,
loc: loc !== null && loc !== void 0 ? loc : undefined,
messageId: "error",
data: { message: error.message },
});
}
function visitUnknown(regexpContext) {
const { node, flags, flagsString, getFlagsLocation } = regexpContext;
const flagSet = new Set();
for (const flag of flagsString !== null && flagsString !== void 0 ? flagsString : "") {
if (flagSet.has(flag)) {
context.report({
node,
loc: getFlagsLocation(),
messageId: "duplicateFlag",
data: { flag },
});
return;
}
flagSet.add(flag);
}
if (flags.unicode && flags.unicodeSets) {
context.report({
node,
loc: getFlagsLocation(),
messageId: "uvFlag",
});
}
}
return (0, utils_1.defineRegexpVisitor)(context, { visitInvalid, visitUnknown });
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,84 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const refa_1 = require("../utils/refa");
exports.default = (0, utils_1.createRule)("no-invisible-character", {
meta: {
docs: {
description: "disallow invisible raw character",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
unexpected: "Unexpected invisible character. Use '{{instead}}' instead.",
},
type: "suggestion",
},
create(context) {
const sourceCode = context.sourceCode;
function createLiteralVisitor({ node, flags, getRegexpLocation, fixReplaceNode, }) {
return {
onCharacterEnter(cNode) {
if (cNode.raw === " ") {
return;
}
if (cNode.raw.length === 1 && (0, utils_1.isInvisible)(cNode.value)) {
const instead = (0, refa_1.toCharSetSource)(cNode.value, flags);
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
data: {
instead,
},
fix: fixReplaceNode(cNode, instead),
});
}
},
};
}
function verifyString({ node, flags }) {
const text = sourceCode.getText(node);
let index = 0;
for (const c of text) {
if (c === " ") {
continue;
}
const cp = c.codePointAt(0);
if ((0, utils_1.isInvisible)(cp)) {
const instead = (0, refa_1.toCharSetSource)(cp, flags);
const range = [
node.range[0] + index,
node.range[0] + index + c.length,
];
context.report({
node,
loc: {
start: sourceCode.getLocFromIndex(range[0]),
end: sourceCode.getLocFromIndex(range[1]),
},
messageId: "unexpected",
data: {
instead,
},
fix(fixer) {
return fixer.replaceTextRange(range, instead);
},
});
}
index += c.length;
}
}
return (0, utils_1.defineRegexpVisitor)(context, {
createLiteralVisitor,
createSourceVisitor(regexpContext) {
if (regexpContext.node.type === "Literal") {
verifyString(regexpContext);
}
return {};
},
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,139 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const get_usage_of_pattern_1 = require("../utils/get-usage-of-pattern");
function* extractLazyEndQuantifiers(alternatives) {
for (const { elements } of alternatives) {
if (elements.length > 0) {
const last = elements[elements.length - 1];
switch (last.type) {
case "Quantifier":
if (!last.greedy && last.min !== last.max) {
yield last;
}
else if (last.max === 1) {
const element = last.element;
if (element.type === "Group" ||
element.type === "CapturingGroup") {
yield* extractLazyEndQuantifiers(element.alternatives);
}
}
break;
case "CapturingGroup":
case "Group":
yield* extractLazyEndQuantifiers(last.alternatives);
break;
default:
break;
}
}
}
}
exports.default = (0, utils_1.createRule)("no-lazy-ends", {
meta: {
docs: {
description: "disallow lazy quantifiers at the end of an expression",
category: "Possible Errors",
recommended: true,
default: "warn",
},
schema: [
{
type: "object",
properties: {
ignorePartial: { type: "boolean" },
},
additionalProperties: false,
},
],
hasSuggestions: true,
messages: {
uselessElement: "The quantifier and the quantified element can be removed because the quantifier is lazy and has a minimum of 0.",
uselessQuantifier: "The quantifier can be removed because the quantifier is lazy and has a minimum of 1.",
uselessRange: "The quantifier can be replaced with '{{{min}}}' because the quantifier is lazy and has a minimum of {{min}}.",
suggestMakeGreedy: "Make the quantifier greedy. (This changes the behavior of the regex.)",
suggestRemoveElement: "Remove the quantified element. (This does not changes the behavior of the regex.)",
suggestRemoveQuantifier: "Remove the quantifier. (This does not changes the behavior of the regex.)",
suggestRange: "Replace the quantifier with '{{{min}}}'. (This does not changes the behavior of the regex.)",
},
type: "problem",
},
create(context) {
var _a, _b;
const ignorePartial = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.ignorePartial) !== null && _b !== void 0 ? _b : true;
function createVisitor({ node, getRegexpLocation, getUsageOfPattern, fixReplaceNode, }) {
if (ignorePartial) {
const usageOfPattern = getUsageOfPattern();
if (usageOfPattern !== get_usage_of_pattern_1.UsageOfPattern.whole) {
return {};
}
}
return {
onPatternEnter(pNode) {
for (const lazy of extractLazyEndQuantifiers(pNode.alternatives)) {
const makeGreedy = {
messageId: "suggestMakeGreedy",
fix: fixReplaceNode(lazy, lazy.raw.slice(0, -1)),
};
if (lazy.min === 0) {
const replacement = pNode.alternatives.length === 1 &&
pNode.alternatives[0].elements.length === 1 &&
pNode.alternatives[0].elements[0] === lazy
? "(?:)"
: "";
context.report({
node,
loc: getRegexpLocation(lazy),
messageId: "uselessElement",
suggest: [
{
messageId: "suggestRemoveElement",
fix: fixReplaceNode(lazy, replacement),
},
makeGreedy,
],
});
}
else if (lazy.min === 1) {
context.report({
node,
loc: getRegexpLocation(lazy),
messageId: "uselessQuantifier",
suggest: [
{
messageId: "suggestRemoveQuantifier",
fix: fixReplaceNode(lazy, lazy.element.raw),
},
makeGreedy,
],
});
}
else {
context.report({
node,
loc: getRegexpLocation(lazy),
messageId: "uselessRange",
data: {
min: String(lazy.min),
},
suggest: [
{
messageId: "suggestRange",
data: {
min: String(lazy.min),
},
fix: fixReplaceNode(lazy, `${lazy.element.raw}{${lazy.min}}`),
},
makeGreedy,
],
});
}
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,107 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const type_tracker_1 = require("../utils/type-tracker");
const eslint_utils_1 = require("@eslint-community/eslint-utils");
const STATIC_PROPERTIES = [
"input",
"$_",
"lastMatch",
"$&",
"lastParen",
"$+",
"leftContext",
"$`",
"rightContext",
"$'",
"$1",
"$2",
"$3",
"$4",
"$5",
"$6",
"$7",
"$8",
"$9",
];
const PROTOTYPE_METHODS = ["compile"];
exports.default = (0, utils_1.createRule)("no-legacy-features", {
meta: {
docs: {
description: "disallow legacy RegExp features",
category: "Best Practices",
recommended: true,
},
schema: [
{
type: "object",
properties: {
staticProperties: {
type: "array",
items: { enum: STATIC_PROPERTIES },
uniqueItems: true,
},
prototypeMethods: {
type: "array",
items: { enum: PROTOTYPE_METHODS },
uniqueItems: true,
},
},
additionalProperties: false,
},
],
messages: {
forbiddenStaticProperty: "'{{name}}' static property is forbidden.",
forbiddenPrototypeMethods: "RegExp.prototype.{{name}} method is forbidden.",
},
type: "suggestion",
},
create(context) {
var _a, _b, _c, _d;
const staticProperties = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.staticProperties) !== null && _b !== void 0 ? _b : STATIC_PROPERTIES;
const prototypeMethods = (_d = (_c = context.options[0]) === null || _c === void 0 ? void 0 : _c.prototypeMethods) !== null && _d !== void 0 ? _d : PROTOTYPE_METHODS;
const typeTracer = (0, type_tracker_1.createTypeTracker)(context);
return {
...(staticProperties.length
? {
Program(program) {
const scope = context.sourceCode.getScope(program);
const tracker = new eslint_utils_1.ReferenceTracker(scope);
const regexpTraceMap = {};
for (const sp of staticProperties) {
regexpTraceMap[sp] = { [eslint_utils_1.READ]: true };
}
for (const { node, path, } of tracker.iterateGlobalReferences({
RegExp: regexpTraceMap,
})) {
context.report({
node,
messageId: "forbiddenStaticProperty",
data: { name: path.join(".") },
});
}
},
}
: {}),
...(prototypeMethods.length
? {
MemberExpression(node) {
if (node.computed ||
node.property.type !== "Identifier" ||
!prototypeMethods.includes(node.property.name) ||
node.object.type === "Super") {
return;
}
if (typeTracer.isRegExp(node.object)) {
context.report({
node,
messageId: "forbiddenPrototypeMethods",
data: { name: node.property.name },
});
}
},
}
: {}),
};
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,305 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const refa_1 = require("refa");
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const fix_simplify_quantifier_1 = require("../utils/fix-simplify-quantifier");
const mention_1 = require("../utils/mention");
const refa_2 = require("../utils/refa");
const regexp_ast_1 = require("../utils/regexp-ast");
const util_1 = require("../utils/util");
function* getStartQuantifiers(root, direction, flags) {
if (Array.isArray(root)) {
for (const a of root) {
yield* getStartQuantifiers(a, direction, flags);
}
return;
}
switch (root.type) {
case "Character":
case "CharacterClass":
case "CharacterSet":
case "ExpressionCharacterClass":
case "Backreference":
break;
case "Assertion":
break;
case "Alternative": {
const elements = direction === "ltr" ? root.elements : (0, util_1.reversed)(root.elements);
for (const e of elements) {
if ((0, regexp_ast_analysis_1.isEmpty)(e, flags))
continue;
yield* getStartQuantifiers(e, direction, flags);
break;
}
break;
}
case "CapturingGroup":
break;
case "Group":
yield* getStartQuantifiers(root.alternatives, direction, flags);
break;
case "Quantifier":
yield root;
if (root.max === 1) {
yield* getStartQuantifiers(root.element, direction, flags);
}
break;
default:
yield (0, util_1.assertNever)(root);
}
}
const getCache = (0, util_1.cachedFn)((_flags) => new WeakMap());
function getSingleRepeatedChar(element, flags, cache = getCache(flags)) {
let value = cache.get(element);
if (value === undefined) {
value = uncachedGetSingleRepeatedChar(element, flags, cache);
cache.set(element, value);
}
return value;
}
function uncachedGetSingleRepeatedChar(element, flags, cache) {
switch (element.type) {
case "Alternative": {
let total = undefined;
for (const e of element.elements) {
const c = getSingleRepeatedChar(e, flags, cache);
if (total === undefined) {
total = c;
}
else {
total = total.intersect(c);
}
if (total.isEmpty)
return total;
}
return total !== null && total !== void 0 ? total : regexp_ast_analysis_1.Chars.empty(flags);
}
case "Assertion":
return regexp_ast_analysis_1.Chars.empty(flags);
case "Backreference":
return regexp_ast_analysis_1.Chars.empty(flags);
case "Character":
case "CharacterClass":
case "CharacterSet":
case "ExpressionCharacterClass": {
const set = (0, regexp_ast_analysis_1.toUnicodeSet)(element, flags);
if (set.accept.isEmpty) {
return set.chars;
}
return set.wordSets
.map((wordSet) => {
let total = undefined;
for (const c of wordSet) {
if (total === undefined) {
total = c;
}
else {
total = total.intersect(c);
}
if (total.isEmpty)
return total;
}
return total !== null && total !== void 0 ? total : regexp_ast_analysis_1.Chars.empty(flags);
})
.reduce((a, b) => a.union(b));
}
case "CapturingGroup":
case "Group":
return element.alternatives
.map((a) => getSingleRepeatedChar(a, flags, cache))
.reduce((a, b) => a.union(b));
case "Quantifier":
if (element.max === 0)
return regexp_ast_analysis_1.Chars.empty(flags);
return getSingleRepeatedChar(element.element, flags, cache);
default:
return (0, util_1.assertNever)(element);
}
}
function getTradingQuantifiersAfter(start, startChar, direction, flags) {
const results = [];
(0, regexp_ast_analysis_1.followPaths)(start, "next", startChar, {
join(states) {
return refa_1.CharSet.empty(startChar.maximum).union(...states);
},
continueAfter(_, state) {
return !state.isEmpty;
},
continueInto(element, state) {
return element.type !== "Assertion" && !state.isEmpty;
},
leave(element, state) {
switch (element.type) {
case "Assertion":
case "Backreference":
case "Character":
case "CharacterClass":
case "CharacterSet":
case "ExpressionCharacterClass":
return state.intersect(getSingleRepeatedChar(element, flags));
case "CapturingGroup":
case "Group":
case "Quantifier":
return state;
default:
return (0, util_1.assertNever)(element);
}
},
enter(element, state) {
if (element.type === "Quantifier" &&
element.min !== element.max) {
const qChar = getSingleRepeatedChar(element, flags);
const intersection = qChar.intersect(state);
if (!intersection.isEmpty) {
results.push({
quant: element,
quantRepeatedChar: qChar,
intersection,
});
}
}
return state;
},
}, direction);
return results;
}
exports.default = (0, utils_1.createRule)("no-misleading-capturing-group", {
meta: {
docs: {
description: "disallow capturing groups that do not behave as one would expect",
category: "Possible Errors",
recommended: true,
},
hasSuggestions: true,
schema: [
{
type: "object",
properties: {
reportBacktrackingEnds: { type: "boolean" },
},
additionalProperties: false,
},
],
messages: {
removeQuant: "{{quant}} can be removed because it is already included by {{cause}}." +
" This makes the capturing group misleading, because it actually captures less text than its pattern suggests.",
replaceQuant: "{{quant}} can be replaced with {{fix}} because of {{cause}}." +
" This makes the capturing group misleading, because it actually captures less text than its pattern suggests.",
suggestionRemove: "Remove {{quant}}.",
suggestionReplace: "Replace {{quant}} with {{fix}}.",
nonAtomic: "The quantifier {{quant}} is not atomic for the characters {{chars}}, so it might capture fewer characters than expected. This makes the capturing group misleading, because the quantifier will capture fewer characters than its pattern suggests in some edge cases.",
suggestionNonAtomic: "Make the quantifier atomic by adding {{fix}}. Careful! This is going to change the behavior of the regex in some edge cases.",
trading: "The quantifier {{quant}} can exchange characters ({{chars}}) with {{other}}. This makes the capturing group misleading, because the quantifier will capture fewer characters than its pattern suggests.",
},
type: "problem",
},
create(context) {
var _a, _b;
const reportBacktrackingEnds = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.reportBacktrackingEnds) !== null && _b !== void 0 ? _b : true;
function createVisitor(regexpContext) {
const { node, flags, getRegexpLocation } = regexpContext;
const parser = (0, refa_2.getParser)(regexpContext);
function reportStartQuantifiers(capturingGroup) {
const direction = (0, regexp_ast_analysis_1.getMatchingDirection)(capturingGroup);
const startQuantifiers = getStartQuantifiers(capturingGroup.alternatives, direction, flags);
for (const quantifier of startQuantifiers) {
const result = (0, regexp_ast_1.canSimplifyQuantifier)(quantifier, flags, parser);
if (!result.canSimplify)
return;
const cause = (0, mention_1.joinEnglishList)(result.dependencies.map((d) => (0, mention_1.mention)(d)));
const [replacement, fix] = (0, fix_simplify_quantifier_1.fixSimplifyQuantifier)(quantifier, result, regexpContext);
if (quantifier.min === 0) {
const removesCapturingGroup = (0, regexp_ast_1.hasCapturingGroup)(quantifier);
context.report({
node,
loc: getRegexpLocation(quantifier),
messageId: "removeQuant",
data: {
quant: (0, mention_1.mention)(quantifier),
cause,
},
suggest: removesCapturingGroup
? undefined
: [
{
messageId: "suggestionRemove",
data: {
quant: (0, mention_1.mention)(quantifier),
},
fix,
},
],
});
}
else {
context.report({
node,
loc: getRegexpLocation(quantifier),
messageId: "replaceQuant",
data: {
quant: (0, mention_1.mention)(quantifier),
fix: (0, mention_1.mention)(replacement),
cause,
},
suggest: [
{
messageId: "suggestionReplace",
data: {
quant: (0, mention_1.mention)(quantifier),
fix: (0, mention_1.mention)(replacement),
},
fix,
},
],
});
}
}
}
function reportTradingEndQuantifiers(capturingGroup) {
const direction = (0, regexp_ast_analysis_1.getMatchingDirection)(capturingGroup);
const endQuantifiers = getStartQuantifiers(capturingGroup.alternatives, (0, regexp_ast_analysis_1.invertMatchingDirection)(direction), flags);
for (const quantifier of endQuantifiers) {
if (!quantifier.greedy) {
continue;
}
if (quantifier.min === quantifier.max) {
continue;
}
const qChar = getSingleRepeatedChar(quantifier, flags);
if (qChar.isEmpty) {
continue;
}
for (const trader of getTradingQuantifiersAfter(quantifier, qChar, direction, flags)) {
if ((0, regexp_ast_analysis_1.hasSomeDescendant)(capturingGroup, trader.quant)) {
continue;
}
if (trader.quant.min >= 1 &&
!(0, regexp_ast_analysis_1.isPotentiallyZeroLength)(trader.quant.element, flags))
context.report({
node,
loc: getRegexpLocation(quantifier),
messageId: "trading",
data: {
quant: (0, mention_1.mention)(quantifier),
other: (0, mention_1.mention)(trader.quant),
chars: (0, refa_2.toCharSetSource)(trader.intersection, flags),
},
});
}
}
}
return {
onCapturingGroupLeave(capturingGroup) {
reportStartQuantifiers(capturingGroup);
if (reportBacktrackingEnds) {
reportTradingEndQuantifiers(capturingGroup);
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,209 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const regex_syntax_1 = require("../utils/regex-syntax");
const segmenter = new Intl.Segmenter();
function startsWithSurrogate(s) {
if (s.length < 2) {
return false;
}
const h = s.charCodeAt(0);
const l = s.charCodeAt(1);
return h >= 0xd800 && h <= 0xdbff && l >= 0xdc00 && l <= 0xdfff;
}
function getProblem(grapheme, flags) {
if (grapheme.length > 2 ||
(grapheme.length === 2 && !startsWithSurrogate(grapheme))) {
return "Multi";
}
else if (!flags.unicode &&
!flags.unicodeSets &&
startsWithSurrogate(grapheme)) {
return "Surrogate";
}
return null;
}
function getGraphemeBeforeQuant(quant) {
const alt = quant.parent;
let start = quant.start;
for (let i = alt.elements.indexOf(quant) - 1; i >= 0; i--) {
const e = alt.elements[i];
if (e.type === "Character" && !(0, regex_syntax_1.isEscapeSequence)(e.raw)) {
start = e.start;
}
else {
break;
}
}
const before = alt.raw.slice(start - alt.start, quant.element.end - alt.start);
const segments = [...segmenter.segment(before)];
const segment = segments[segments.length - 1];
return segment.segment;
}
function getGraphemeProblems(cc, flags) {
const offset = cc.negate ? 2 : 1;
const ignoreElements = cc.elements.filter((element) => element.type === "CharacterClass" ||
element.type === "ExpressionCharacterClass" ||
element.type === "ClassStringDisjunction");
const problems = [];
for (const { segment, index } of segmenter.segment(cc.raw.slice(offset, -1))) {
const problem = getProblem(segment, flags);
if (problem !== null) {
const start = offset + index + cc.start;
const end = start + segment.length;
if (ignoreElements.some((ignore) => ignore.start <= start && end <= ignore.end)) {
continue;
}
problems.push({
grapheme: segment,
problem,
start,
end,
elements: cc.elements.filter((e) => e.start < end && e.end > start),
});
}
}
return problems;
}
function getGraphemeProblemsFix(problems, cc, flags) {
if (cc.negate) {
return null;
}
if (!problems.every((p) => p.start === p.elements[0].start &&
p.end === p.elements[p.elements.length - 1].end)) {
return null;
}
const prefixGraphemes = problems.map((p) => p.grapheme);
let ccRaw = cc.raw;
for (let i = problems.length - 1; i >= 0; i--) {
const { start, end } = problems[i];
ccRaw = ccRaw.slice(0, start - cc.start) + ccRaw.slice(end - cc.start);
}
if (flags.unicodeSets) {
const prefix = prefixGraphemes.join("|");
return `[\\q{${prefix}}${ccRaw.slice(1, -1)}]`;
}
if (ccRaw.startsWith("[^")) {
ccRaw = `[\\${ccRaw.slice(1)}`;
}
const prefix = prefixGraphemes.sort((a, b) => b.length - a.length).join("|");
let fix = prefix;
let singleAlternative = problems.length === 1;
if (ccRaw !== "[]") {
fix += `|${ccRaw}`;
singleAlternative = false;
}
if (singleAlternative && cc.parent.type === "Alternative") {
return fix;
}
if (cc.parent.type === "Alternative" && cc.parent.elements.length === 1) {
return fix;
}
return `(?:${fix})`;
}
exports.default = (0, utils_1.createRule)("no-misleading-unicode-character", {
meta: {
docs: {
description: "disallow multi-code-point characters in character classes and quantifiers",
category: "Possible Errors",
recommended: true,
},
schema: [
{
type: "object",
properties: {
fixable: { type: "boolean" },
},
additionalProperties: false,
},
],
fixable: "code",
hasSuggestions: true,
messages: {
characterClass: "The character(s) {{ graphemes }} are all represented using multiple {{ unit }}.{{ uFlag }}",
quantifierMulti: "The character {{ grapheme }} is represented using multiple Unicode code points. The quantifier only applies to the last code point {{ last }} and not to the whole character.",
quantifierSurrogate: "The character {{ grapheme }} is represented using a surrogate pair. The quantifier only applies to the tailing surrogate {{ last }} and not to the whole character.",
fixCharacterClass: "Move the character(s) {{ graphemes }} outside the character class.",
fixQuantifier: "Wrap a group around {{ grapheme }}.",
},
type: "problem",
},
create(context) {
var _a, _b;
const fixable = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.fixable) !== null && _b !== void 0 ? _b : false;
function makeFix(fix, messageId, data) {
if (fixable) {
return { fix };
}
return {
suggest: [{ messageId, data, fix }],
};
}
function createVisitor(regexpContext) {
const { node, patternSource, flags, getRegexpLocation, fixReplaceNode, } = regexpContext;
return {
onCharacterClassEnter(ccNode) {
const problems = getGraphemeProblems(ccNode, flags);
if (problems.length === 0) {
return;
}
const range = {
start: problems[0].start,
end: problems[problems.length - 1].end,
};
const fix = getGraphemeProblemsFix(problems, ccNode, flags);
const graphemes = problems
.map((p) => (0, mention_1.mention)(p.grapheme))
.join(", ");
const uFlag = problems.every((p) => p.problem === "Surrogate");
context.report({
node,
loc: getRegexpLocation(range),
messageId: "characterClass",
data: {
graphemes,
unit: flags.unicode || flags.unicodeSets
? "code points"
: "char codes",
uFlag: uFlag ? " Use the `u` flag." : "",
},
...makeFix(fixReplaceNode(ccNode, () => fix), "fixCharacterClass", { graphemes }),
});
},
onQuantifierEnter(qNode) {
if (qNode.element.type !== "Character") {
return;
}
const grapheme = getGraphemeBeforeQuant(qNode);
const problem = getProblem(grapheme, flags);
if (problem === null) {
return;
}
context.report({
node,
loc: getRegexpLocation(qNode),
messageId: `quantifier${problem}`,
data: {
grapheme: (0, mention_1.mention)(grapheme),
last: (0, mention_1.mentionChar)(qNode.element),
},
...makeFix((fixer) => {
const range = patternSource.getReplaceRange({
start: qNode.element.end - grapheme.length,
end: qNode.element.end,
});
if (!range) {
return null;
}
return range.replace(fixer, `(?:${grapheme})`);
}, "fixQuantifier", { grapheme: (0, mention_1.mention)(grapheme) }),
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,97 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const ast_utils_1 = require("../utils/ast-utils");
const type_tracker_1 = require("../utils/type-tracker");
function parseOption(userOption) {
let strictTypes = true;
if (userOption) {
if (userOption.strictTypes != null) {
strictTypes = userOption.strictTypes;
}
}
return {
strictTypes,
};
}
exports.default = (0, utils_1.createRule)("no-missing-g-flag", {
meta: {
docs: {
description: "disallow missing `g` flag in patterns used in `String#matchAll` and `String#replaceAll`",
category: "Possible Errors",
recommended: true,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
strictTypes: { type: "boolean" },
},
additionalProperties: false,
},
],
messages: {
missingGlobalFlag: "The pattern given to the argument of `String#{{method}}()` requires the `g` flag, but is missing it.",
},
type: "problem",
},
create(context) {
const { strictTypes } = parseOption(context.options[0]);
const typeTracer = (0, type_tracker_1.createTypeTracker)(context);
function visit(regexpContext) {
const { regexpNode, flags, flagsString } = regexpContext;
if (flags.global ||
flagsString == null) {
return;
}
for (const ref of (0, ast_utils_1.extractExpressionReferences)(regexpNode, context)) {
verifyExpressionReference(ref, regexpContext);
}
}
function verifyExpressionReference(ref, { regexpNode, fixReplaceFlags, flagsString, }) {
if (ref.type !== "argument") {
return;
}
const node = ref.callExpression;
if (node.arguments[0] !== ref.node ||
!(0, ast_utils_1.isKnownMethodCall)(node, {
matchAll: 1,
replaceAll: 2,
})) {
return;
}
if (strictTypes
? !typeTracer.isString(node.callee.object)
: !typeTracer.maybeString(node.callee.object)) {
return;
}
context.report({
node: ref.node,
messageId: "missingGlobalFlag",
data: {
method: node.callee.property.name,
},
fix: buildFixer(),
});
function buildFixer() {
if (node.arguments[0] !== regexpNode ||
((regexpNode.type === "NewExpression" ||
regexpNode.type === "CallExpression") &&
regexpNode.arguments[1] &&
regexpNode.arguments[1].type !== "Literal")) {
return null;
}
return fixReplaceFlags(`${flagsString}g`, false);
}
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor(regexpContext) {
visit(regexpContext);
return {};
},
visitInvalid: visit,
visitUnknown: visit,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const STANDARD_FLAGS = "dgimsuvy";
exports.default = (0, utils_1.createRule)("no-non-standard-flag", {
meta: {
docs: {
description: "disallow non-standard flags",
category: "Best Practices",
recommended: true,
},
schema: [],
messages: {
unexpected: "Unexpected non-standard flag '{{flag}}'.",
},
type: "suggestion",
},
create(context) {
function visit({ regexpNode, getFlagsLocation, flagsString, }) {
if (flagsString) {
const nonStandard = [...flagsString].filter((f) => !STANDARD_FLAGS.includes(f));
if (nonStandard.length > 0) {
context.report({
node: regexpNode,
loc: getFlagsLocation(),
messageId: "unexpected",
data: { flag: nonStandard[0] },
});
}
}
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor(regexpContext) {
visit(regexpContext);
return {};
},
visitInvalid: visit,
visitUnknown: visit,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,68 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const char_ranges_1 = require("../utils/char-ranges");
const mention_1 = require("../utils/mention");
const regex_syntax_1 = require("../utils/regex-syntax");
exports.default = (0, utils_1.createRule)("no-obscure-range", {
meta: {
docs: {
description: "disallow obscure character ranges",
category: "Best Practices",
recommended: true,
},
schema: [
{
type: "object",
properties: {
allowed: (0, char_ranges_1.getAllowedCharValueSchema)(),
},
additionalProperties: false,
},
],
messages: {
unexpected: "Unexpected obscure character range. The characters of {{range}} are not obvious.",
},
type: "suggestion",
},
create(context) {
var _a;
const allowedRanges = (0, char_ranges_1.getAllowedCharRanges)((_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.allowed, context);
function createVisitor({ node, getRegexpLocation, }) {
return {
onCharacterClassRangeEnter(rNode) {
const { min, max } = rNode;
if (min.value === max.value) {
return;
}
if ((0, regex_syntax_1.isControlEscape)(min.raw) && (0, regex_syntax_1.isControlEscape)(max.raw)) {
return;
}
if ((0, regex_syntax_1.isOctalEscape)(min.raw) && (0, regex_syntax_1.isOctalEscape)(max.raw)) {
return;
}
if (((0, regex_syntax_1.isHexLikeEscape)(min.raw) || min.value === 0) &&
(0, regex_syntax_1.isHexLikeEscape)(max.raw)) {
return;
}
if (!(0, regex_syntax_1.isEscapeSequence)(min.raw) &&
!(0, regex_syntax_1.isEscapeSequence)(max.raw) &&
(0, char_ranges_1.inRange)(allowedRanges, min.value, max.value)) {
return;
}
context.report({
node,
loc: getRegexpLocation(rNode),
messageId: "unexpected",
data: {
range: (0, mention_1.mentionChar)(rNode),
},
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,60 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const regex_syntax_1 = require("../utils/regex-syntax");
exports.default = (0, utils_1.createRule)("no-octal", {
meta: {
docs: {
description: "disallow octal escape sequence",
category: "Best Practices",
recommended: false,
},
schema: [],
messages: {
unexpected: "Unexpected octal escape sequence '{{expr}}'.",
replaceHex: "Replace the octal escape sequence with a hexadecimal escape sequence.",
},
type: "suggestion",
hasSuggestions: true,
},
create(context) {
function createVisitor({ node, fixReplaceNode, getRegexpLocation, }) {
return {
onCharacterEnter(cNode) {
if (cNode.raw === "\\0") {
return;
}
if (!(0, regex_syntax_1.isOctalEscape)(cNode.raw)) {
return;
}
const report = cNode.raw.startsWith("\\0") ||
!(cNode.parent.type === "CharacterClass" ||
cNode.parent.type === "CharacterClassRange");
if (report) {
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
data: {
expr: cNode.raw,
},
suggest: [
{
messageId: "replaceHex",
fix: fixReplaceNode(cNode, () => {
return `\\x${cNode.value
.toString(16)
.padStart(2, "0")}`;
}),
},
],
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,81 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
function isZeroQuantifier(node) {
return node.min === 0;
}
function isOptional(assertion, quantifier, flags) {
let element = assertion;
while (element.parent !== quantifier) {
const parent = element.parent;
if (parent.type === "Alternative") {
for (const e of parent.elements) {
if (e === element) {
continue;
}
if (!(0, regexp_ast_analysis_1.isZeroLength)(e, flags)) {
return false;
}
}
if (parent.parent.type === "Pattern") {
throw new Error("The given assertion is not a descendant of the given quantifier.");
}
element = parent.parent;
}
else {
if (parent.max > 1 && !(0, regexp_ast_analysis_1.isZeroLength)(parent, flags)) {
return false;
}
element = parent;
}
}
return true;
}
exports.default = (0, utils_1.createRule)("no-optional-assertion", {
meta: {
docs: {
description: "disallow optional assertions",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
optionalAssertion: "This assertion effectively optional and does not change the pattern. Either remove the assertion or change the parent quantifier '{{quantifier}}'.",
},
type: "problem",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
const zeroQuantifierStack = [];
return {
onQuantifierEnter(q) {
if (isZeroQuantifier(q)) {
zeroQuantifierStack.unshift(q);
}
},
onQuantifierLeave(q) {
if (zeroQuantifierStack[0] === q) {
zeroQuantifierStack.shift();
}
},
onAssertionEnter(assertion) {
const q = zeroQuantifierStack[0];
if (q && isOptional(assertion, q, flags)) {
context.report({
node,
loc: getRegexpLocation(assertion),
messageId: "optionalAssertion",
data: {
quantifier: q.raw.substr(q.element.raw.length),
},
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-potentially-useless-backreference", {
meta: {
docs: {
description: "disallow backreferences that reference a group that might not be matched",
category: "Possible Errors",
recommended: true,
default: "warn",
},
schema: [],
messages: {
potentiallyUselessBackreference: "Some paths leading to the backreference do not go through the referenced capturing group or the captured text might be reset before reaching the backreference.",
},
type: "problem",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
return {
onBackreferenceEnter(backreference) {
if ((0, regexp_ast_analysis_1.isEmptyBackreference)(backreference, flags)) {
return;
}
if (!(0, regexp_ast_analysis_1.isStrictBackreference)(backreference)) {
context.report({
node,
loc: getRegexpLocation(backreference),
messageId: "potentiallyUselessBackreference",
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,35 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
exports.default = (0, utils_1.createRule)("no-standalone-backslash", {
meta: {
docs: {
description: "disallow standalone backslashes (`\\`)",
category: "Best Practices",
recommended: false,
},
schema: [],
messages: {
unexpected: "Unexpected standalone backslash (`\\`). It looks like an escape sequence, but it's a single `\\` character pattern.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, getRegexpLocation, }) {
return {
onCharacterEnter(cNode) {
if (cNode.value === utils_1.CP_BACK_SLASH && cNode.raw === "\\") {
context.report({
node,
loc: getRegexpLocation(cNode),
messageId: "unexpected",
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,105 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const scslre_1 = require("scslre");
const utils_1 = require("../utils");
const get_usage_of_pattern_1 = require("../utils/get-usage-of-pattern");
const mention_1 = require("../utils/mention");
const refa_1 = require("../utils/refa");
function unionLocations(a, b) {
function less(x, y) {
if (x.line < y.line) {
return true;
}
else if (x.line > y.line) {
return false;
}
return x.column < y.column;
}
return {
start: { ...(less(a.start, b.start) ? a.start : b.start) },
end: { ...(less(a.end, b.end) ? b.end : a.end) },
};
}
exports.default = (0, utils_1.createRule)("no-super-linear-backtracking", {
meta: {
docs: {
description: "disallow exponential and polynomial backtracking",
category: "Possible Errors",
recommended: true,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
report: {
enum: ["certain", "potential"],
},
},
additionalProperties: false,
},
],
messages: {
self: "This quantifier can reach itself via the loop {{parent}}." +
" Using any string accepted by {{attack}}, this can be exploited to cause at least polynomial backtracking." +
"{{exp}}",
trade: "The quantifier {{start}} can exchange characters with {{end}}." +
" Using any string accepted by {{attack}}, this can be exploited to cause at least polynomial backtracking." +
"{{exp}}",
},
type: "problem",
},
create(context) {
var _a, _b;
const reportUncertain = ((_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.report) !== null && _b !== void 0 ? _b : "certain") ===
"potential";
function createVisitor(regexpContext) {
const { node, patternAst, flags, getRegexpLocation, fixReplaceNode, getUsageOfPattern, } = regexpContext;
const result = (0, scslre_1.analyse)((0, refa_1.getJSRegexppAst)(regexpContext), {
reportTypes: { Move: false },
assumeRejectingSuffix: reportUncertain &&
getUsageOfPattern() !== get_usage_of_pattern_1.UsageOfPattern.whole,
});
for (const report of result.reports) {
const exp = report.exponential
? " This is going to cause exponential backtracking resulting in exponential worst-case runtime behavior."
: getUsageOfPattern() !== get_usage_of_pattern_1.UsageOfPattern.whole
? " This might cause exponential backtracking."
: "";
const attack = `/${report.character.literal.source}+/${flags.ignoreCase ? "i" : ""}`;
const fix = fixReplaceNode(patternAst, () => { var _a, _b; return (_b = (_a = report.fix()) === null || _a === void 0 ? void 0 : _a.source) !== null && _b !== void 0 ? _b : null; });
if (report.type === "Self") {
context.report({
node,
loc: getRegexpLocation(report.quant),
messageId: "self",
data: {
exp,
attack,
parent: (0, mention_1.mention)(report.parentQuant),
},
fix,
});
}
else if (report.type === "Trade") {
context.report({
node,
loc: unionLocations(getRegexpLocation(report.startQuant), getRegexpLocation(report.endQuant)),
messageId: "trade",
data: {
exp,
attack,
start: (0, mention_1.mention)(report.startQuant),
end: (0, mention_1.mention)(report.endQuant),
},
fix,
});
}
}
return {};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,194 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const refa_1 = require("refa");
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const scslre_1 = require("scslre");
const utils_1 = require("../utils");
const get_usage_of_pattern_1 = require("../utils/get-usage-of-pattern");
const refa_2 = require("../utils/refa");
function dedupeReports(reports) {
const seen = new Set();
const result = [];
for (const r of reports) {
if (!seen.has(r.quant)) {
result.push(r);
seen.add(r.quant);
}
}
return result;
}
function* findReachableQuantifiers(node, flags) {
switch (node.type) {
case "CapturingGroup":
case "Group":
case "Pattern": {
for (const a of node.alternatives) {
yield* findReachableQuantifiers(a, flags);
}
break;
}
case "Assertion": {
if (node.kind === "lookahead" || node.kind === "lookbehind") {
for (const a of node.alternatives) {
yield* findReachableQuantifiers(a, flags);
}
}
break;
}
case "Quantifier": {
yield node;
break;
}
case "Alternative": {
const dir = (0, regexp_ast_analysis_1.getMatchingDirection)(node);
for (let i = 0; i < node.elements.length; i++) {
const elementIndex = dir === "ltr" ? i : node.elements.length - 1 - i;
const element = node.elements[elementIndex];
yield* findReachableQuantifiers(element, flags);
if (!(0, regexp_ast_analysis_1.isPotentiallyEmpty)(element, flags)) {
break;
}
}
break;
}
default:
break;
}
}
const TRANSFORMER_OPTIONS = {
ignoreAmbiguity: true,
ignoreOrder: true,
};
const PASS_1 = refa_1.Transformers.simplify(TRANSFORMER_OPTIONS);
const PASS_2 = new refa_1.CombinedTransformer([
refa_1.Transformers.inline(TRANSFORMER_OPTIONS),
refa_1.Transformers.removeDeadBranches(TRANSFORMER_OPTIONS),
refa_1.Transformers.replaceAssertions({
...TRANSFORMER_OPTIONS,
replacement: "empty-set",
}),
]);
exports.default = (0, utils_1.createRule)("no-super-linear-move", {
meta: {
docs: {
description: "disallow quantifiers that cause quadratic moves",
category: "Possible Errors",
recommended: false,
},
schema: [
{
type: "object",
properties: {
report: {
enum: ["certain", "potential"],
},
ignoreSticky: {
type: "boolean",
},
ignorePartial: {
type: "boolean",
},
},
additionalProperties: false,
},
],
messages: {
unexpected: "Any attack string {{attack}} plus some rejecting suffix will cause quadratic runtime because of this quantifier.",
},
type: "problem",
},
create(context) {
var _a, _b, _c, _d, _e, _f;
const reportUncertain = ((_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.report) !== null && _b !== void 0 ? _b : "certain") ===
"potential";
const ignoreSticky = (_d = (_c = context.options[0]) === null || _c === void 0 ? void 0 : _c.ignoreSticky) !== null && _d !== void 0 ? _d : true;
const ignorePartial = (_f = (_e = context.options[0]) === null || _e === void 0 ? void 0 : _e.ignorePartial) !== null && _f !== void 0 ? _f : true;
function getScslreReports(regexpContext, assumeRejectingSuffix) {
const { flags } = regexpContext;
const result = (0, scslre_1.analyse)((0, refa_2.getJSRegexppAst)(regexpContext, true), {
reportTypes: { Move: true, Self: false, Trade: false },
assumeRejectingSuffix,
});
return result.reports.map((r) => {
if (r.type !== "Move") {
throw new Error("Unexpected report type");
}
return {
quant: r.quant,
attack: `/${r.character.literal.source}+/${flags.ignoreCase ? "i" : ""}`,
};
});
}
function* getSimpleReports(regexpContext, assumeRejectingSuffix) {
const { patternAst, flags } = regexpContext;
const parser = refa_1.JS.Parser.fromAst((0, refa_2.getJSRegexppAst)(regexpContext, true));
for (const q of findReachableQuantifiers(patternAst, flags)) {
if (q.max !== Infinity) {
continue;
}
if (q.element.type === "Assertion" ||
q.element.type === "Backreference") {
continue;
}
let e = parser.parseElement(q.element, {
assertions: "parse",
backreferences: "disable",
}).expression;
e = (0, refa_1.transform)(PASS_1, e);
e = (0, refa_1.transform)(PASS_2, e);
if (e.alternatives.length === 0) {
continue;
}
let hasCharacters = false;
(0, refa_1.visitAst)(e, {
onCharacterClassEnter() {
hasCharacters = true;
},
});
if (!hasCharacters) {
continue;
}
if (!assumeRejectingSuffix) {
const after = (0, regexp_ast_analysis_1.getFirstConsumedCharAfter)(q, (0, regexp_ast_analysis_1.getMatchingDirection)(q), flags);
if (after.empty && after.look.char.isAll) {
continue;
}
}
const attack = `/${refa_1.JS.toLiteral({
type: "Quantifier",
alternatives: e.alternatives,
min: 1,
max: Infinity,
lazy: false,
}).source}/${flags.ignoreCase ? "i" : ""}`;
yield { quant: q, attack };
}
}
function createVisitor(regexpContext) {
const { node, flags, getRegexpLocation, getUsageOfPattern } = regexpContext;
if (ignoreSticky && flags.sticky) {
return {};
}
const usage = getUsageOfPattern();
if (ignorePartial && usage === get_usage_of_pattern_1.UsageOfPattern.partial) {
return {};
}
const assumeRejectingSuffix = reportUncertain && usage !== get_usage_of_pattern_1.UsageOfPattern.whole;
for (const report of dedupeReports([
...getSimpleReports(regexpContext, assumeRejectingSuffix),
...getScslreReports(regexpContext, assumeRejectingSuffix),
])) {
context.report({
node,
loc: getRegexpLocation(report.quant),
messageId: "unexpected",
data: { attack: report.attack },
});
}
return {};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
function isLookaround(node) {
return (node.type === "Assertion" &&
(node.kind === "lookahead" || node.kind === "lookbehind"));
}
function getTriviallyNestedAssertion(node) {
const alternatives = node.alternatives;
if (alternatives.length === 1) {
const elements = alternatives[0].elements;
if (elements.length === 1) {
const element = elements[0];
if (element.type === "Assertion") {
return element;
}
}
}
return null;
}
function getNegatedRaw(assertion) {
if (assertion.kind === "word") {
return assertion.negate ? "\\b" : "\\B";
}
else if (assertion.kind === "lookahead") {
return `(?${assertion.negate ? "=" : "!"}${assertion.raw.slice(3)}`;
}
else if (assertion.kind === "lookbehind") {
return `(?<${assertion.negate ? "=" : "!"}${assertion.raw.slice(4)}`;
}
return null;
}
exports.default = (0, utils_1.createRule)("no-trivially-nested-assertion", {
meta: {
docs: {
description: "disallow trivially nested assertions",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
unexpected: "Unexpected trivially nested assertion.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, fixReplaceNode, getRegexpLocation, }) {
return {
onAssertionEnter(aNode) {
if (aNode.parent.type === "Quantifier") {
return;
}
if (!isLookaround(aNode)) {
return;
}
const nested = getTriviallyNestedAssertion(aNode);
if (nested === null) {
return;
}
if (aNode.negate &&
isLookaround(nested) &&
nested.negate &&
(0, regexp_ast_analysis_1.hasSomeDescendant)(nested, (d) => d.type === "CapturingGroup")) {
return;
}
const replacement = aNode.negate
? getNegatedRaw(nested)
: nested.raw;
if (replacement === null) {
return;
}
context.report({
node,
loc: getRegexpLocation(aNode),
messageId: "unexpected",
fix: fixReplaceNode(aNode, replacement),
});
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,141 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const regexp_ast_1 = require("../utils/regexp-ast");
function getCombinedQuant(parent, child) {
if (parent.max === 0 || child.max === 0) {
return null;
}
else if (parent.greedy === child.greedy) {
const greedy = parent.greedy;
const a = child.min;
const b = child.max;
const c = parent.min;
const d = parent.max;
const condition = b === Infinity && c === 0
? a <= 1
: c === d || b * c + 1 >= a * (c + 1);
if (condition) {
return {
min: a * c,
max: b * d,
greedy,
};
}
return null;
}
return null;
}
function getSimplifiedChildQuant(parent, child) {
if (parent.max === 0 || child.max === 0) {
return null;
}
else if (parent.greedy !== child.greedy) {
return null;
}
let min = child.min;
let max = child.max;
if (min === 0 && parent.min === 0) {
min = 1;
}
if (parent.max === Infinity && (min === 0 || min === 1) && max > 1) {
max = 1;
}
return { min, max, greedy: child.greedy };
}
function isTrivialQuantifier(quant) {
return quant.min === quant.max && (quant.min === 0 || quant.min === 1);
}
function* iterateSingleQuantifiers(group) {
for (const { elements } of group.alternatives) {
if (elements.length === 1) {
const single = elements[0];
if (single.type === "Quantifier") {
yield single;
}
}
}
}
exports.default = (0, utils_1.createRule)("no-trivially-nested-quantifier", {
meta: {
docs: {
description: "disallow nested quantifiers that can be rewritten as one quantifier",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [],
messages: {
nested: "These two quantifiers are trivially nested and can be replaced with '{{quant}}'.",
childOne: "This nested quantifier can be removed.",
childSimpler: "This nested quantifier can be simplified to '{{quant}}'.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, fixReplaceNode, fixReplaceQuant, getRegexpLocation, }) {
return {
onQuantifierEnter(qNode) {
if (isTrivialQuantifier(qNode)) {
return;
}
const element = qNode.element;
if (element.type !== "Group") {
return;
}
for (const child of iterateSingleQuantifiers(element)) {
if (isTrivialQuantifier(child)) {
continue;
}
if (element.alternatives.length === 1) {
const quant = getCombinedQuant(qNode, child);
if (!quant) {
continue;
}
const quantStr = (0, regexp_ast_1.quantToString)(quant);
const replacement = child.element.raw + quantStr;
context.report({
node,
loc: getRegexpLocation(qNode),
messageId: "nested",
data: { quant: quantStr },
fix: fixReplaceNode(qNode, replacement),
});
}
else {
const quant = getSimplifiedChildQuant(qNode, child);
if (!quant) {
continue;
}
if (quant.min === child.min &&
quant.max === child.max) {
continue;
}
if (quant.min === 1 && quant.max === 1) {
context.report({
node,
loc: getRegexpLocation(child),
messageId: "childOne",
fix: fixReplaceNode(child, child.element.raw),
});
}
else {
quant.greedy = undefined;
context.report({
node,
loc: getRegexpLocation(child),
messageId: "childSimpler",
data: { quant: (0, regexp_ast_1.quantToString)(quant) },
fix: fixReplaceQuant(child, quant),
});
}
}
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,151 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
function getCapturingGroupIdentifier(group) {
if (group.name) {
return `'${group.name}'`;
}
return `number ${(0, regexp_ast_analysis_1.getCapturingGroupNumber)(group)}`;
}
exports.default = (0, utils_1.createRule)("no-unused-capturing-group", {
meta: {
docs: {
description: "disallow unused capturing group",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
fixable: { type: "boolean" },
allowNamed: { type: "boolean" },
},
additionalProperties: false,
},
],
messages: {
unusedCapturingGroup: "Capturing group {{identifier}} is defined but never used.",
makeNonCapturing: "Making this a non-capturing group.",
},
type: "suggestion",
hasSuggestions: true,
},
create(context) {
var _a, _b, _c, _d;
const fixable = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.fixable) !== null && _b !== void 0 ? _b : false;
const allowNamed = (_d = (_c = context.options[0]) === null || _c === void 0 ? void 0 : _c.allowNamed) !== null && _d !== void 0 ? _d : false;
function reportUnused(unused, regexpContext) {
const { node, getRegexpLocation, fixReplaceNode, getAllCapturingGroups, } = regexpContext;
if (allowNamed) {
for (const cgNode of unused) {
if (cgNode.name) {
unused.delete(cgNode);
}
}
}
const fixableGroups = new Set();
for (const group of [...getAllCapturingGroups()].reverse()) {
if (unused.has(group)) {
fixableGroups.add(group);
}
else {
break;
}
}
for (const cgNode of unused) {
const fix = fixableGroups.has(cgNode)
? fixReplaceNode(cgNode, cgNode.raw.replace(/^\((?:\?<[^<>]+>)?/u, "(?:"))
: null;
context.report({
node,
loc: getRegexpLocation(cgNode),
messageId: "unusedCapturingGroup",
data: { identifier: getCapturingGroupIdentifier(cgNode) },
fix: fixable ? fix : null,
suggest: fix
? [{ messageId: "makeNonCapturing", fix }]
: null,
});
}
}
function getCapturingGroupReferences(regexpContext) {
const capturingGroupReferences = regexpContext.getCapturingGroupReferences();
if (!capturingGroupReferences.length) {
return null;
}
const indexRefs = [];
const namedRefs = [];
let hasUnknownName = false;
let hasSplit = false;
for (const ref of capturingGroupReferences) {
if (ref.type === "UnknownUsage" || ref.type === "UnknownRef") {
return null;
}
if (ref.type === "ArrayRef" ||
ref.type === "ReplacementRef" ||
ref.type === "ReplacerFunctionRef") {
if (ref.kind === "index") {
if (ref.ref != null) {
indexRefs.push(ref.ref);
}
else {
return null;
}
}
else {
if (ref.ref) {
namedRefs.push(ref.ref);
}
else {
hasUnknownName = true;
}
}
}
else if (ref.type === "Split") {
hasSplit = true;
}
}
return {
unusedIndexRef(index) {
if (hasSplit) {
return false;
}
return !indexRefs.includes(index);
},
unusedNamedRef(name) {
if (hasUnknownName) {
return false;
}
return !namedRefs.includes(name);
},
};
}
function createVisitor(regexpContext) {
const references = getCapturingGroupReferences(regexpContext);
if (!references) {
return {};
}
const unused = new Set();
const allCapturingGroups = regexpContext.getAllCapturingGroups();
for (let index = 0; index < allCapturingGroups.length; index++) {
const cgNode = allCapturingGroups[index];
if (cgNode.references.length ||
!references.unusedIndexRef(index + 1)) {
continue;
}
if (cgNode.name && !references.unusedNamedRef(cgNode.name)) {
continue;
}
unused.add(cgNode);
}
reportUnused(unused, regexpContext);
return {};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,378 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
const util_1 = require("../utils/util");
function containsAssertion(n) {
return (0, regexp_ast_analysis_1.hasSomeDescendant)(n, (d) => d.type === "Assertion");
}
function isSingleCharacterAssertion(assertion, direction, flags) {
switch (assertion.kind) {
case "word":
return false;
case "start":
return direction === "rtl";
case "end":
return direction === "ltr";
default:
break;
}
if ((0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind) !== direction) {
return false;
}
return assertion.alternatives.every((alt) => {
if (!containsAssertion(alt)) {
const range = (0, regexp_ast_analysis_1.getLengthRange)(alt, flags);
return range.min === 1 && range.max === 1;
}
let consumed = false;
let asserted = false;
const elements = direction === "ltr" ? alt.elements : [...alt.elements].reverse();
for (const e of elements) {
if (!consumed) {
if (e.type === "Assertion" &&
isSingleCharacterAssertion(e, direction, flags)) {
asserted = true;
continue;
}
if (containsAssertion(e)) {
return false;
}
const range = (0, regexp_ast_analysis_1.getLengthRange)(e, flags);
if (range.max === 0) {
continue;
}
else if (range.min === 1 && range.max === 1) {
consumed = true;
}
else {
return false;
}
}
else {
const otherDir = (0, regexp_ast_analysis_1.invertMatchingDirection)(direction);
if (e.type === "Assertion" &&
isSingleCharacterAssertion(e, otherDir, flags)) {
continue;
}
return false;
}
}
return consumed || asserted;
});
}
function firstLookCharsIntersection(a, b) {
const char = a.char.intersect(b.char);
return {
char: a.char.intersect(b.char),
exact: (a.exact && b.exact) || char.isEmpty,
edge: a.edge && b.edge,
};
}
function createReorderingGetFirstCharAfter(forbidden) {
function hasForbidden(element) {
if (element.type === "Assertion" && forbidden.has(element)) {
return true;
}
for (const f of forbidden) {
if ((0, regexp_ast_analysis_1.hasSomeDescendant)(element, f)) {
return true;
}
}
return false;
}
return (afterThis, direction, flags) => {
let result = (0, regexp_ast_analysis_1.getFirstCharAfter)(afterThis, direction, flags);
if (afterThis.parent.type === "Alternative") {
const { elements } = afterThis.parent;
const inc = direction === "ltr" ? -1 : +1;
const start = elements.indexOf(afterThis);
for (let i = start + inc; i >= 0 && i < elements.length; i += inc) {
const other = elements[i];
if (!(0, regexp_ast_analysis_1.isZeroLength)(other, flags)) {
break;
}
if (hasForbidden(other)) {
break;
}
const otherResult = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_analysis_1.getFirstConsumedChar)(other, direction, flags));
result = firstLookCharsIntersection(result, otherResult);
}
}
return result;
};
}
function removeAlternative(alternative) {
const parent = alternative.parent;
if (parent.alternatives.length > 1) {
let { start, end } = alternative;
if (parent.alternatives[0] === alternative) {
end++;
}
else {
start--;
}
const before = parent.raw.slice(0, start - parent.start);
const after = parent.raw.slice(end - parent.start);
return [parent, before + after];
}
switch (parent.type) {
case "Pattern":
return [parent, "[]"];
case "Assertion": {
const assertionParent = parent.parent;
if (parent.negate) {
return [
assertionParent.type === "Quantifier"
? assertionParent
: parent,
"",
];
}
if (assertionParent.type === "Quantifier") {
if (assertionParent.min === 0) {
return [assertionParent, ""];
}
return removeAlternative(assertionParent.parent);
}
return removeAlternative(assertionParent);
}
case "CapturingGroup": {
const before = parent.raw.slice(0, alternative.start - parent.start);
const after = parent.raw.slice(alternative.end - parent.start);
return [parent, `${before}[]${after}`];
}
case "Group": {
const groupParent = parent.parent;
if (groupParent.type === "Quantifier") {
if (groupParent.min === 0) {
return [groupParent, ""];
}
return removeAlternative(groupParent.parent);
}
return removeAlternative(groupParent);
}
default:
return (0, util_1.assertNever)(parent);
}
}
const messages = {
alwaysRejectByChar: "{{assertion}} will always reject because it is {{followedOrPreceded}} by a character.",
alwaysAcceptByChar: "{{assertion}} will always accept because it is never {{followedOrPreceded}} by a character.",
alwaysRejectByNonLineTerminator: "{{assertion}} will always reject because it is {{followedOrPreceded}} by a non-line-terminator character.",
alwaysAcceptByLineTerminator: "{{assertion}} will always accept because it is {{followedOrPreceded}} by a line-terminator character.",
alwaysAcceptByLineTerminatorOrEdge: "{{assertion}} will always accept because it is {{followedOrPreceded}} by a line-terminator character or the {{startOrEnd}} of the input string.",
alwaysAcceptOrRejectFollowedByWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a non-word character and followed by a word character.",
alwaysAcceptOrRejectFollowedByNonWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a non-word character and followed by a non-word character.",
alwaysAcceptOrRejectPrecededByWordFollowedByNonWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a word character and followed by a non-word character.",
alwaysAcceptOrRejectPrecededByWordFollowedByWord: "{{assertion}} will always {{acceptOrReject}} because it is preceded by a word character and followed by a word character.",
alwaysForLookaround: "The {{kind}} {{assertion}} will always {{acceptOrReject}}.",
alwaysForNegativeLookaround: "The negative {{kind}} {{assertion}} will always {{acceptOrReject}}.",
acceptSuggestion: "Remove the assertion. (Replace with empty string.)",
rejectSuggestion: "Remove branch of the assertion. (Replace with empty set.)",
};
exports.default = (0, utils_1.createRule)("no-useless-assertions", {
meta: {
docs: {
description: "disallow assertions that are known to always accept (or reject)",
category: "Possible Errors",
recommended: true,
},
hasSuggestions: true,
schema: [],
messages,
type: "problem",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, fixReplaceNode, }) {
const reported = new Set();
function replaceWithEmptyString(assertion) {
if (assertion.parent.type === "Quantifier") {
return fixReplaceNode(assertion.parent, "");
}
return fixReplaceNode(assertion, "");
}
function replaceWithEmptySet(assertion) {
if (assertion.parent.type === "Quantifier") {
if (assertion.parent.min === 0) {
return fixReplaceNode(assertion.parent, "");
}
const [element, replacement] = removeAlternative(assertion.parent.parent);
return fixReplaceNode(element, replacement);
}
const [element, replacement] = removeAlternative(assertion.parent);
return fixReplaceNode(element, replacement);
}
function report(assertion, messageId, data) {
reported.add(assertion);
const { acceptOrReject } = data;
context.report({
node,
loc: getRegexpLocation(assertion),
messageId,
data: {
assertion: (0, mention_1.mention)(assertion),
...data,
},
suggest: [
{
messageId: `${acceptOrReject}Suggestion`,
fix: acceptOrReject === "accept"
? replaceWithEmptyString(assertion)
: replaceWithEmptySet(assertion),
},
],
});
}
function verifyStartOrEnd(assertion, getFirstCharAfterFn) {
const direction = (0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind);
const next = getFirstCharAfterFn(assertion, direction, flags);
const followedOrPreceded = assertion.kind === "end" ? "followed" : "preceded";
const lineTerminator = regexp_ast_analysis_1.Chars.lineTerminator(flags);
if (next.edge) {
if (!flags.multiline) {
if (next.char.isEmpty) {
report(assertion, "alwaysAcceptByChar", {
followedOrPreceded,
acceptOrReject: "accept",
});
}
}
else {
if (next.char.isSubsetOf(lineTerminator)) {
report(assertion, "alwaysAcceptByLineTerminatorOrEdge", {
followedOrPreceded,
startOrEnd: assertion.kind,
acceptOrReject: "accept",
});
}
}
}
else {
if (!flags.multiline) {
report(assertion, "alwaysRejectByChar", {
followedOrPreceded,
acceptOrReject: "reject",
});
}
else {
if (next.char.isDisjointWith(lineTerminator)) {
report(assertion, "alwaysRejectByNonLineTerminator", {
followedOrPreceded,
acceptOrReject: "reject",
});
}
else if (next.char.isSubsetOf(lineTerminator)) {
report(assertion, "alwaysAcceptByLineTerminator", {
followedOrPreceded,
acceptOrReject: "accept",
});
}
}
}
}
function verifyWordBoundary(assertion, getFirstCharAfterFn) {
const word = regexp_ast_analysis_1.Chars.word(flags);
const next = getFirstCharAfterFn(assertion, "ltr", flags);
const prev = getFirstCharAfterFn(assertion, "rtl", flags);
const nextIsWord = next.char.isSubsetOf(word) && !next.edge;
const prevIsWord = prev.char.isSubsetOf(word) && !prev.edge;
const nextIsNonWord = next.char.isDisjointWith(word);
const prevIsNonWord = prev.char.isDisjointWith(word);
const accept = assertion.negate ? "reject" : "accept";
const reject = assertion.negate ? "accept" : "reject";
if (prevIsNonWord) {
if (nextIsWord) {
report(assertion, "alwaysAcceptOrRejectFollowedByWord", {
acceptOrReject: accept,
});
}
if (nextIsNonWord) {
report(assertion, "alwaysAcceptOrRejectFollowedByNonWord", {
acceptOrReject: reject,
});
}
}
if (prevIsWord) {
if (nextIsNonWord) {
report(assertion, "alwaysAcceptOrRejectPrecededByWordFollowedByNonWord", {
acceptOrReject: accept,
});
}
if (nextIsWord) {
report(assertion, "alwaysAcceptOrRejectPrecededByWordFollowedByWord", {
acceptOrReject: reject,
});
}
}
}
function verifyLookaround(assertion, getFirstCharAfterFn) {
if ((0, regexp_ast_analysis_1.isPotentiallyEmpty)(assertion.alternatives, flags)) {
return;
}
const direction = (0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind);
const after = getFirstCharAfterFn(assertion, direction, flags);
const firstOf = regexp_ast_analysis_1.FirstConsumedChars.toLook((0, regexp_ast_analysis_1.getFirstConsumedChar)(assertion.alternatives, direction, flags));
const accept = assertion.negate ? "reject" : "accept";
const reject = assertion.negate ? "accept" : "reject";
if (after.char.isDisjointWith(firstOf.char) &&
!(after.edge && firstOf.edge)) {
report(assertion, assertion.negate
? "alwaysForNegativeLookaround"
: "alwaysForLookaround", {
kind: assertion.kind,
acceptOrReject: reject,
});
}
const edgeSubset = firstOf.edge || !after.edge;
if (firstOf.exact &&
edgeSubset &&
after.char.isSubsetOf(firstOf.char) &&
isSingleCharacterAssertion(assertion, (0, regexp_ast_analysis_1.getMatchingDirectionFromAssertionKind)(assertion.kind), flags)) {
report(assertion, assertion.negate
? "alwaysForNegativeLookaround"
: "alwaysForLookaround", {
kind: assertion.kind,
acceptOrReject: accept,
});
}
}
function verifyAssertion(assertion, getFirstCharAfterFn) {
switch (assertion.kind) {
case "start":
case "end":
verifyStartOrEnd(assertion, getFirstCharAfterFn);
break;
case "word":
verifyWordBoundary(assertion, getFirstCharAfterFn);
break;
case "lookahead":
case "lookbehind":
verifyLookaround(assertion, getFirstCharAfterFn);
break;
default:
throw (0, util_1.assertNever)(assertion);
}
}
const allAssertions = [];
return {
onAssertionEnter(assertion) {
verifyAssertion(assertion, regexp_ast_analysis_1.getFirstCharAfter);
allAssertions.push(assertion);
},
onPatternLeave() {
const reorderingGetFirstCharAfter = createReorderingGetFirstCharAfter(reported);
for (const assertion of allAssertions) {
if (!reported.has(assertion)) {
verifyAssertion(assertion, reorderingGetFirstCharAfter);
}
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,116 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const regexp_ast_analysis_1 = require("regexp-ast-analysis");
const utils_1 = require("../utils");
const mention_1 = require("../utils/mention");
function hasNegatedLookaroundInBetween(from, to) {
for (let p = from.parent; p && p !== to; p = p.parent) {
if (p.type === "Assertion" &&
(p.kind === "lookahead" || p.kind === "lookbehind") &&
p.negate) {
return true;
}
}
return false;
}
function getUselessProblem(backRef, flags) {
const groups = [backRef.resolved].flat();
const problems = [];
for (const group of groups) {
const messageId = getUselessMessageId(backRef, group, flags);
if (!messageId) {
return null;
}
problems.push({ messageId, group });
}
if (problems.length === 0) {
return null;
}
let problemsToReport;
const problemsInSameDisjunction = problems.filter((problem) => problem.messageId !== "disjunctive");
if (problemsInSameDisjunction.length) {
problemsToReport = problemsInSameDisjunction;
}
else {
problemsToReport = problems;
}
const [{ messageId, group }, ...other] = problemsToReport;
let otherGroups = "";
if (other.length === 1) {
otherGroups = " and another group";
}
else if (other.length > 1) {
otherGroups = ` and other ${other.length} groups`;
}
return {
messageId,
group,
otherGroups,
};
}
function getUselessMessageId(backRef, group, flags) {
const closestAncestor = (0, regexp_ast_analysis_1.getClosestAncestor)(backRef, group);
if (closestAncestor === group) {
return "nested";
}
else if (closestAncestor.type !== "Alternative") {
return "disjunctive";
}
if (hasNegatedLookaroundInBetween(group, closestAncestor)) {
return "intoNegativeLookaround";
}
const matchingDir = (0, regexp_ast_analysis_1.getMatchingDirection)(closestAncestor);
if (matchingDir === "ltr" && backRef.end <= group.start) {
return "forward";
}
else if (matchingDir === "rtl" && group.end <= backRef.start) {
return "backward";
}
if ((0, regexp_ast_analysis_1.isZeroLength)(group, flags)) {
return "empty";
}
return null;
}
exports.default = (0, utils_1.createRule)("no-useless-backreference", {
meta: {
docs: {
description: "disallow useless backreferences in regular expressions",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
nested: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} from within that group.",
forward: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} which appears later in the pattern.",
backward: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} which appears before in the same lookbehind.",
disjunctive: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} which is in another alternative.",
intoNegativeLookaround: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} which is in a negative lookaround.",
empty: "Backreference {{ bref }} will be ignored. It references group {{ group }}{{ otherGroups }} which always captures zero characters.",
},
type: "suggestion",
},
create(context) {
function createVisitor({ node, flags, getRegexpLocation, }) {
return {
onBackreferenceEnter(backRef) {
const problem = getUselessProblem(backRef, flags);
if (problem) {
context.report({
node,
loc: getRegexpLocation(backRef),
messageId: problem.messageId,
data: {
bref: (0, mention_1.mention)(backRef),
group: (0, mention_1.mention)(problem.group),
otherGroups: problem.otherGroups,
},
});
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,212 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const regex_syntax_1 = require("../utils/regex-syntax");
const ESCAPES_OUTSIDE_CHARACTER_CLASS = new Set("$()*+./?[{|");
const ESCAPES_OUTSIDE_CHARACTER_CLASS_WITH_U = new Set([
...ESCAPES_OUTSIDE_CHARACTER_CLASS,
"}",
]);
exports.default = (0, utils_1.createRule)("no-useless-character-class", {
meta: {
docs: {
description: "disallow character class with one character",
category: "Best Practices",
recommended: true,
},
fixable: "code",
schema: [
{
type: "object",
properties: {
ignores: {
type: "array",
items: {
type: "string",
minLength: 1,
},
uniqueItems: true,
},
},
additionalProperties: false,
},
],
messages: {
unexpectedCharacterClassWith: "Unexpected character class with one {{type}}. Can remove brackets{{additional}}.",
unexpectedUnnecessaryNestingCharacterClass: "Unexpected unnecessary nesting character class. Can remove brackets.",
},
type: "suggestion",
},
create(context) {
var _a, _b;
const ignores = (_b = (_a = context.options[0]) === null || _a === void 0 ? void 0 : _a.ignores) !== null && _b !== void 0 ? _b : ["="];
function createVisitor({ node, pattern, flags, fixReplaceNode, getRegexpLocation, }) {
const characterClassStack = [];
return {
onExpressionCharacterClassEnter(eccNode) {
characterClassStack.push(eccNode);
},
onExpressionCharacterClassLeave() {
characterClassStack.pop();
},
onCharacterClassEnter(ccNode) {
characterClassStack.push(ccNode);
},
onCharacterClassLeave(ccNode) {
var _a, _b;
characterClassStack.pop();
if (ccNode.negate) {
return;
}
let messageId, messageData;
const unwrapped = ccNode.elements.map((_e, index) => {
var _a, _b;
const element = ccNode.elements[index];
return ((_b = (_a = (index === 0
? getEscapedFirstRawIfNeeded(element)
: null)) !== null && _a !== void 0 ? _a : (index === ccNode.elements.length - 1
? getEscapedLastRawIfNeeded(element)
: null)) !== null && _b !== void 0 ? _b : element.raw);
});
if (ccNode.elements.length !== 1 &&
ccNode.parent.type === "CharacterClass") {
messageId = "unexpectedUnnecessaryNestingCharacterClass";
messageData = {
type: "unnecessary nesting character class",
};
if (!ccNode.elements.length) {
const nextElement = ccNode.parent.elements[ccNode.parent.elements.indexOf(ccNode) + 1];
if (nextElement &&
isNeedEscapedForFirstElement(nextElement)) {
unwrapped.push("\\");
}
}
}
else {
if (ccNode.elements.length !== 1) {
return;
}
const element = ccNode.elements[0];
if (ignores.length > 0 &&
ignores.includes(element.raw)) {
return;
}
if (element.type === "Character") {
if (element.raw === "\\b") {
return;
}
if (/^\\\d+$/u.test(element.raw) &&
!element.raw.startsWith("\\0")) {
return;
}
if (ignores.length > 0 &&
ignores.includes(String.fromCodePoint(element.value))) {
return;
}
if (!(0, utils_1.canUnwrapped)(ccNode, element.raw)) {
return;
}
messageData = { type: "character" };
}
else if (element.type === "CharacterClassRange") {
if (element.min.value !== element.max.value) {
return;
}
messageData = {
type: "character class range",
additional: " and range",
};
unwrapped[0] =
(_b = (_a = getEscapedFirstRawIfNeeded(element.min)) !== null && _a !== void 0 ? _a : getEscapedLastRawIfNeeded(element.min)) !== null && _b !== void 0 ? _b : element.min.raw;
}
else if (element.type === "ClassStringDisjunction") {
if (!characterClassStack.length) {
return;
}
messageData = { type: "string literal" };
}
else if (element.type === "CharacterSet") {
messageData = { type: "character class escape" };
}
else if (element.type === "CharacterClass" ||
element.type === "ExpressionCharacterClass") {
messageData = { type: "character class" };
}
else {
return;
}
messageId = "unexpectedCharacterClassWith";
}
context.report({
node,
loc: getRegexpLocation(ccNode),
messageId,
data: {
type: messageData.type,
additional: messageData.additional || "",
},
fix: fixReplaceNode(ccNode, unwrapped.join("")),
});
function isNeedEscapedForFirstElement(element) {
const char = element.type === "Character"
? element.raw
: element.type === "CharacterClassRange"
? element.min.raw
: null;
if (char == null) {
return false;
}
if (characterClassStack.length) {
if (regex_syntax_1.RESERVED_DOUBLE_PUNCTUATOR_CHARS.has(char) &&
pattern[ccNode.start - 1] === char) {
return true;
}
return (char === "^" &&
ccNode.parent.type === "CharacterClass" &&
ccNode.parent.elements[0] === ccNode);
}
return (flags.unicode
? ESCAPES_OUTSIDE_CHARACTER_CLASS_WITH_U
: ESCAPES_OUTSIDE_CHARACTER_CLASS).has(char);
}
function needEscapedForLastElement(element) {
const char = element.type === "Character"
? element.raw
: element.type === "CharacterClassRange"
? element.max.raw
: null;
if (char == null) {
return false;
}
if (characterClassStack.length) {
return (regex_syntax_1.RESERVED_DOUBLE_PUNCTUATOR_CHARS.has(char) &&
pattern[ccNode.end] === char);
}
return false;
}
function getEscapedFirstRawIfNeeded(firstElement) {
if (isNeedEscapedForFirstElement(firstElement)) {
return `\\${firstElement.raw}`;
}
return null;
}
function getEscapedLastRawIfNeeded(lastElement) {
if (needEscapedForLastElement(lastElement)) {
const lastRaw = lastElement.type === "Character"
? lastElement.raw
: lastElement.type === "CharacterClassRange"
? lastElement.max.raw
: "";
const prefix = lastElement.raw.slice(0, -lastRaw.length);
return `${prefix}\\${lastRaw}`;
}
return null;
}
},
};
}
return (0, utils_1.defineRegexpVisitor)(context, {
createVisitor,
});
},
});

View file

@ -0,0 +1,2 @@
declare const _default: import("../types").RuleModule;
export default _default;

View file

@ -0,0 +1,91 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const utils_1 = require("../utils");
const ast_utils_1 = require("../utils/ast-utils");
const regexp_ast_1 = require("../utils/regexp-ast");
const type_tracker_1 = require("../utils/type-tracker");
function extractDollarReplacements(context, node) {
return (0, ast_utils_1.parseReplacements)(context, node).filter((e) => e.type === "ReferenceElement");
}
exports.default = (0, utils_1.createRule)("no-useless-dollar-replacements", {
meta: {
docs: {
description: "disallow useless `$` replacements in replacement string",
category: "Possible Errors",
recommended: true,
},
schema: [],
messages: {
numberRef: "'${{ refText }}' replacement will insert '${{ refText }}' because there are less than {{ num }} capturing groups. Use '$$' if you want to escape '$'.",
numberRefCapturingNotFound: "'${{ refText }}' replacement will insert '${{ refText }}' because capturing group is not found. Use '$$' if you want to escape '$'.",
namedRef: "'$<{{ refText }}>' replacement will be ignored because the named capturing group is not found. Use '$$' if you want to escape '$'.",
namedRefNamedCapturingNotFound: "'$<{{ refText }}>' replacement will insert '$<{{ refText }}>' because named capturing group is not found. Use '$$' if you want to escape '$'.",
},
type: "suggestion",
},
create(context) {
const typeTracer = (0, type_tracker_1.createTypeTracker)(context);
const sourceCode = context.sourceCode;
function verify(patternNode, replacement) {
const captures = (0, regexp_ast_1.extractCaptures)(patternNode);
for (const dollarReplacement of extractDollarReplacements(context, replacement)) {
if (typeof dollarReplacement.ref === "number") {
if (captures.count < dollarReplacement.ref) {
context.report({
node: replacement,
loc: {
start: sourceCode.getLocFromIndex(dollarReplacement.range[0]),
end: sourceCode.getLocFromIndex(dollarReplacement.range[1]),
},
messageId: captures.count > 0
? "numberRef"
: "numberRefCapturingNotFound",
data: {
refText: dollarReplacement.refText,
num: String(dollarReplacement.ref),
},
});
}
}
else {
if (!captures.names.has(dollarReplacement.ref)) {
context.report({
node: replacement,
loc: {
start: sourceCode.getLocFromIndex(dollarReplacement.range[0]),
end: sourceCode.getLocFromIndex(dollarReplacement.range[1]),
},
messageId: captures.names.size > 0
? "namedRef"
: "namedRefNamedCapturingNotFound",
data: {
refText: dollarReplacement.refText,
},
});
}
}
}
}
return {
CallExpression(node) {
if (!(0, ast_utils_1.isKnownMethodCall)(node, { replace: 2, replaceAll: 2 })) {
return;
}
const mem = node.callee;
const replacementTextNode = node.arguments[1];
if (replacementTextNode.type !== "Literal" ||
typeof replacementTextNode.value !== "string") {
return;
}
const patternNode = (0, regexp_ast_1.getRegExpNodeFromExpression)(node.arguments[0], context);
if (!patternNode) {
return;
}
if (!typeTracer.isString(mem.object)) {
return;
}
verify(patternNode, replacementTextNode);
},
};
},
});

Some files were not shown because too many files have changed in this diff Show more