diff --git a/.eslintrc.json b/.eslintrc.json index 668bbad52298f..e1c2609d2e2a4 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -144,7 +144,8 @@ "local/no-in-operator": "error", "local/debug-assert": "error", "local/no-keywords": "error", - "local/jsdoc-format": "error" + "local/jsdoc-format": "error", + "local/bounds-check": "error" }, "overrides": [ // By default, the ESLint CLI only looks at .js files. But, it will also look at diff --git a/scripts/eslint/rules/bounds-check.cjs b/scripts/eslint/rules/bounds-check.cjs new file mode 100644 index 0000000000000..20e402e108927 --- /dev/null +++ b/scripts/eslint/rules/bounds-check.cjs @@ -0,0 +1,603 @@ +/** @import { TSESTree } from "@typescript-eslint/types" */ +const { AST_NODE_TYPES } = require("@typescript-eslint/utils"); +const { createRule } = require("./utils.cjs"); + +module.exports = createRule({ + name: "bounds-check", + meta: { + docs: { + description: ``, + }, + messages: { + codePointUncheckedError: `Use 'codePointChecked' instead of 'codePointUnchecked'`, + codePointAtError: `Use 'codePointChecked' instead of 'text.codePointAt'`, + charCodeUncheckedError: `Use 'charCodeChecked' instead of 'charCodeUnchecked'`, + charCodeAtError: `Use 'charCodeChecked' instead of 'text.charCodeAt'`, + }, + schema: [], + type: "problem", + }, + defaultOptions: [], + create(context) { + if (!context.filename.endsWith("scanner.ts")) { + return {}; + } + + return { + CallExpression(node) { + if ( + node.callee.type === AST_NODE_TYPES.MemberExpression && + node.callee.object.type === AST_NODE_TYPES.Identifier && + node.callee.object.name === "text" && + !node.callee.computed + ) { + const name = node.callee.property.name; + switch (name) { + case "charCodeAt": + case "codePointAt": { + /** @type {TSESTree.Node | undefined} */ + let n = node.parent; + while (n) { + if (n.type === AST_NODE_TYPES.FunctionDeclaration && n.id?.name === "createScanner") { + context.report({ messageId: `${name}Error`, node: node.callee }); + break; + } + n = n.parent; + } + break; + } + } + } + else if (node.callee.type === AST_NODE_TYPES.Identifier) { + switch (node.callee.name) { + case "codePointUnchecked": + case "charCodeUnchecked": { + // perform a rudimentary test to determine if there is a definite bounds check for this node + if (!hasDefiniteBoundsCheck(node)) { + context.report({ messageId: `${node.callee.name}Error`, node: node.callee }); + } + break; + } + } + } + }, + }; + }, +}); + +/** + * @param {TSESTree.CallExpression} node + * @returns {boolean} + */ +function hasDefiniteBoundsCheck(node) { + if (node.arguments.length === 0) { + return false; + } + + let index; + let offset = 0; + const arg = node.arguments[0]; + switch (arg.type) { + case AST_NODE_TYPES.Identifier: { + // pos + index = arg.name; + break; + } + case AST_NODE_TYPES.BinaryExpression: { + switch (arg.operator) { + case "+": { + const { left, right } = arg; + if ( + left.type === AST_NODE_TYPES.Identifier && + right.type === AST_NODE_TYPES.Literal && + typeof right.value === "number" + ) { + // pos + 1 + index = left.name; + offset = right.value; + } + else if ( + right.type === AST_NODE_TYPES.Identifier && + left.type === AST_NODE_TYPES.Literal && + typeof left.value === "number" + ) { + // 1 + pos + index = right.name; + offset = left.value; + } + else { + return false; + } + break; + } + case "-": { + const { left, right } = arg; + if ( + left.type === AST_NODE_TYPES.Identifier && + right.type === AST_NODE_TYPES.Literal && + typeof right.value === "number" + ) { + // pos - 1 + index = left.name; + offset = -right.value; + } + else { + return false; + } + break; + } + default: { + return false; + } + } + break; + } + default: { + return false; + } + } + + /** @type {TSESTree.Node} */ + let n = node; + let negated = false; + while (n.parent) { + switch (n.parent.type) { + case AST_NODE_TYPES.UnaryExpression: { + if (n.parent.operator === "!") { + negated = !negated; + } + else { + return false; + } + break; + } + case AST_NODE_TYPES.LogicalExpression: { + switch (n.parent.operator) { + case "&&": { + const { left, right } = n.parent; + switch (n) { + case left: { + break; + } + case right: { + if (inBounds(left, index, offset, negated)) { + return true; + } + if (containsSideEffect(left, index)) { + return false; + } + break; + } + } + break; + } + case "||": + case "??": { + const { left, right } = n.parent; + switch (n) { + case left: { + break; + } + case right: { + if (inBounds(left, index, offset, !negated)) { + return true; + } + if (containsSideEffect(left, index)) { + return false; + } + break; + } + } + break; + } + default: { + return false; + } + } + break; + } + case AST_NODE_TYPES.ConditionalExpression: { + const { test, consequent, alternate } = n.parent; + switch (n) { + case test: { + break; + } + case consequent: { + if (inBounds(test, index, offset, negated)) { + return true; + } + if (containsSideEffect(test, index)) { + return false; + } + break; + } + case alternate: { + if (inBounds(test, index, offset, !negated)) { + return true; + } + if (containsSideEffect(test, index)) { + return false; + } + break; + } + } + break; + } + case AST_NODE_TYPES.BinaryExpression: + case AST_NODE_TYPES.AssignmentExpression: { + const { left, right } = n.parent; + switch (n) { + case left: { + break; + } + case right: { + if (containsSideEffect(left, index)) { + return false; + } + break; + } + } + break; + } + case AST_NODE_TYPES.SequenceExpression: { + const { expressions } = n.parent; + const i = expressions.indexOf(/** @type {TSESTree.Expression} */ (n)); + if (expressions.slice(0, i).some(n => containsSideEffect(n, index))) { + return false; + } + break; + } + case AST_NODE_TYPES.CallExpression: { + const args = n.parent.arguments; + const i = args.indexOf(/** @type {*} */ (n)); + if (args.slice(0, i).some(n => containsSideEffect(n, index))) { + return false; + } + break; + } + case AST_NODE_TYPES.WhileStatement: { + const { test, body } = n.parent; + switch (n) { + case test: { + break; + } + case body: { + if (inBounds(test, index, offset, negated)) { + return true; + } + if (containsSideEffect(test, index)) { + return false; + } + break; + } + } + break; + } + case AST_NODE_TYPES.IfStatement: { + const { test, consequent, alternate } = n.parent; + switch (n) { + case test: { + break; + } + case consequent: { + if (inBounds(test, index, offset, negated)) { + return true; + } + if (containsSideEffect(test, index)) { + return false; + } + break; + } + case alternate: { + if (inBounds(test, index, offset, !negated)) { + return true; + } + if (containsSideEffect(test, index)) { + return false; + } + break; + } + } + break; + } + case AST_NODE_TYPES.BlockStatement: { + const [first] = n.parent.body; + switch (n) { + case first: { + break; + } + default: { + return false; + } + } + break; + } + case AST_NODE_TYPES.ExpressionStatement: { + break; + } + case AST_NODE_TYPES.VariableDeclarator: { + const { id, init } = n.parent; + switch (n) { + case /** @type {TSESTree.Node} */ (id): + break; + case init: + if (containsSideEffect(n, index)) { + return false; + } + break; + } + break; + } + case AST_NODE_TYPES.VariableDeclaration: { + const declarations = /** @type {readonly TSESTree.Node[]} */ (n.parent.declarations); + const i = declarations.indexOf(n); + if (declarations.slice(0, i).some(n => containsSideEffect(n, index))) { + return false; + } + break; + } + default: { + return false; + } + } + n = n.parent; + } + + return false; +} + +/** + * @param {TSESTree.Node} node + * @param {string} index + * @returns {boolean} + */ +function containsSideEffect(node, index) { + /** + * @param {TSESTree.Node} node + * @returns {boolean} + */ + function contains(node) { + switch (node.type) { + case AST_NODE_TYPES.PrivateIdentifier: + case AST_NODE_TYPES.Literal: + case AST_NODE_TYPES.Identifier: { + return false; + } + case AST_NODE_TYPES.MemberExpression: { + const { object, computed, property } = node; + return contains(object) || computed && contains(property); + } + case AST_NODE_TYPES.UpdateExpression: { + const { argument } = node; + return argument.type === AST_NODE_TYPES.Identifier && (argument.name === index || argument.name === "end"); + } + case AST_NODE_TYPES.AssignmentExpression: { + const { left, right } = node; + return left.type === AST_NODE_TYPES.Identifier && (left.name === index || left.name === "end") || contains(right); + } + case AST_NODE_TYPES.CallExpression: { + const { callee, arguments: args } = node; + if (callee.type === AST_NODE_TYPES.Identifier) { + switch (callee.name) { + case "codePointChecked": + case "codePointUnchecked": + case "charCodeChecked": + case "charCodeUnchecked": { + return args.some(contains); + } + } + + // assumes functions named `is*` do not cause side effects + if (/^is[A-Z]/.test(callee.name)) { + return args.some(contains); + } + } + break; // assume every other call introduces potential side effects + } + case AST_NODE_TYPES.LogicalExpression: + case AST_NODE_TYPES.BinaryExpression: { + const { left, right } = node; + return contains(left) || contains(right); + } + case AST_NODE_TYPES.ConditionalExpression: { + const { test, consequent, alternate } = node; + return contains(test) || contains(consequent) || contains(alternate); + } + case AST_NODE_TYPES.UnaryExpression: { + return contains(node.argument); + } + case AST_NODE_TYPES.SequenceExpression: { + return node.expressions.some(contains); + } + case AST_NODE_TYPES.VariableDeclaration: { + return node.declarations.some(contains); + } + case AST_NODE_TYPES.VariableDeclarator: { + const { id, init } = node; + return contains(id) || !!init && contains(init); + } + } + return true; // assume everything else has potential side effects + } + + return contains(node); +} + +/** + * @typedef {"in-bounds" | "out-of-bounds" | false} Bounds + * + * @param {TSESTree.Expression} node + * @param {string} index + * @param {number} offset + * @param {boolean} negated + * @returns {boolean} + */ +function inBounds(node, index, offset, negated) { + const result = checkBounds(node, index, offset); + return result === (negated ? "out-of-bounds" : "in-bounds"); +} + +/** + * @param {TSESTree.Expression} node + * @param {string} index + * @param {number} offset + * @returns {Bounds} + */ +function checkBounds(node, index, offset) { + /** + * @param {TSESTree.Node} node + * @returns {Bounds} + */ + function check(node) { + switch (node.type) { + case AST_NODE_TYPES.UnaryExpression: { + const { operator, argument } = node; + if (operator === "!") { + const result = check(argument); + if (result === "in-bounds") return "out-of-bounds"; + if (result === "out-of-bounds") return "in-bounds"; + } + break; + } + case AST_NODE_TYPES.LogicalExpression: { + const { left, operator, right } = node; + if (operator === "&&") { + const first = check(left); + if (first === "in-bounds") return "in-bounds"; + const second = check(right); + if (second === "in-bounds") return "in-bounds"; + return first || second; + } + break; + } + case AST_NODE_TYPES.BinaryExpression: { + const { left, operator, right } = node; + switch (operator) { + case "<": { + switch (true) { + case isBound(left, index, offset, op.GE) && isBound(right, "end"): + case isBound(left, index) && isBound(right, "end", -offset, op.LE): + return "in-bounds"; + case isBound(left, "end", -offset, op.LE) && isBound(right, index): + case isBound(left, "end") && isBound(right, index, offset, op.GE): + return "out-of-bounds"; + } + break; + } + case ">": { + const { left, right } = node; + switch (true) { + case isBound(left, index, offset, op.GE) && isBound(right, "end"): + case isBound(left, index) && isBound(right, "end", -offset, op.LE): + return "out-of-bounds"; + case isBound(left, "end", -offset, op.LE) && isBound(right, index): + case isBound(left, "end") && isBound(right, index, offset, op.GE): + return "in-bounds"; + } + break; + } + case "<=": { + const { left, right } = node; + switch (true) { + case isBound(right, index, offset) && isBound(left, "end"): + case isBound(right, index) && isBound(left, "end", -offset): + return "out-of-bounds"; + } + break; + } + case ">=": { + const { left, right } = node; + switch (true) { + case isBound(left, index, offset) && isBound(right, "end"): + case isBound(left, index) && isBound(right, "end", -offset): + return "out-of-bounds"; + } + break; + } + } + break; + } + case AST_NODE_TYPES.SequenceExpression: { + return check(node.expressions[node.expressions.length - 1]); + } + } + return false; + } + + if (containsSideEffect(node, index)) { + return false; + } + + return check(node); +} + +/** + * @param {TSESTree.Node} node + * @param {string} name + * @param {number} offset + * @param {(a: number, b: number) => boolean} [cmp] + * @returns {boolean} + */ +function isBound(node, name, offset = 0, cmp = op.EQ) { + switch (node.type) { + case AST_NODE_TYPES.Identifier: { + return node.name === name && offset === 0; + } + case AST_NODE_TYPES.BinaryExpression: { + const { left, operator, right } = node; + switch (operator) { + case "+": { + if (left.type === AST_NODE_TYPES.Identifier) { + return left.name === name && right.type === AST_NODE_TYPES.Literal && typeof right.value === "number" && cmp(right.value, offset); + } + if (right.type === AST_NODE_TYPES.Identifier) { + return right.name === name && left.type === AST_NODE_TYPES.Literal && typeof left.value === "number" && cmp(left.value, offset); + } + break; + } + case "-": { + if (left.type === AST_NODE_TYPES.Identifier) { + switch (cmp) { + case op.GE: + cmp = op.LE; + break; + case op.LE: + cmp = op.GE; + break; + } + return left.name === name && right.type === AST_NODE_TYPES.Literal && typeof right.value === "number" && cmp(right.value, -offset); + } + break; + } + } + break; + } + } + return false; +} + +const op = { + /** + * @param {number} a + * @param {number} b + */ + EQ(a, b) { + return a === b; + }, + + /** + * @param {number} a + * @param {number} b + */ + LE(a, b) { + return a <= b; + }, + + /** + * @param {number} a + * @param {number} b + */ + GE(a, b) { + return a >= b; + }, +}; diff --git a/scripts/eslint/tests/bounds-check.test.cjs b/scripts/eslint/tests/bounds-check.test.cjs new file mode 100644 index 0000000000000..c230efd2f5e00 --- /dev/null +++ b/scripts/eslint/tests/bounds-check.test.cjs @@ -0,0 +1,203 @@ +const { RuleTester } = require("./support/RuleTester.cjs"); +const rule = require("../rules/bounds-check.cjs"); + +const ruleTester = new RuleTester({ + parserOptions: { + warnOnUnsupportedTypeScriptVersion: false, + }, + parser: require.resolve("@typescript-eslint/parser"), +}); + +ruleTester.run("bounds-check", rule, { + valid: [ + { + filename: "scanner.ts", + code: `codePointChecked(0)`, + }, + { + filename: "scanner.ts", + code: `charCodeChecked(0)`, + }, + { + filename: "foo.ts", + code: `codePointUnchecked(0)`, + }, + { + filename: "foo.ts", + code: `charCodeUnchecked(0)`, + }, + { + filename: "scanner.ts", + code: "pos < end && charCodeUnchecked(pos)", + }, + { + filename: "scanner.ts", + code: "!(pos >= end) && charCodeUnchecked(pos)", + }, + { + filename: "scanner.ts", + code: "pos + 1 < end && charCodeUnchecked(pos)", + }, + { + filename: "scanner.ts", + code: "pos >= end || charCodeUnchecked(pos)", + }, + { + filename: "scanner.ts", + code: "pos < end ? charCodeUnchecked(pos) : null", + }, + { + filename: "scanner.ts", + code: "pos >= end ? null : charCodeUnchecked(pos)", + }, + { + filename: "scanner.ts", + code: "if (pos < end) charCodeUnchecked(pos)", + }, + { + filename: "scanner.ts", + code: "if (pos < end) {charCodeUnchecked(pos)}", + }, + { + filename: "scanner.ts", + code: "if (pos >= end); else charCodeUnchecked(pos)", + }, + { + filename: "scanner.ts", + code: "if (pos >= end); else {charCodeUnchecked(pos)}", + }, + { + filename: "scanner.ts", + code: "while (pos < end) charCodeUnchecked(pos)", + }, + { + filename: "scanner.ts", + code: "while (pos < end) {charCodeUnchecked(pos)}", + }, + { + filename: "scanner.ts", + code: "return pos >= 0 && pos < end ? codePointUnchecked(pos) : CharacterCodes.EOF;", + }, + { + filename: "scanner.ts", + code: ` + if ( + pos + 2 < end && + codePointUnchecked(pos + 1) === CharacterCodes.u && + codePointUnchecked(pos + 2) === CharacterCodes.openBrace + ) {} + `, + }, + { + filename: "scanner.ts", + code: ` + while (pos < end) { + let ch = codePointUnchecked(pos); + } + `, + }, + { + filename: "scanner.ts", + code: ` + while (pos < end && isWhiteSpaceSingleLine(charCodeUnchecked(pos)) && isWhiteSpaceSingleLine(charCodeUnchecked(pos))) {}`, + }, + ], + + invalid: [ + { + filename: "scanner.ts", + code: `codePointUnchecked(0)`, + errors: [{ messageId: "codePointUncheckedError" }], + }, + { + filename: "scanner.ts", + code: `charCodeUnchecked(0)`, + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: `pos < end && charCodeUnchecked(pos + 1)`, + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: `!(pos < end) && charCodeUnchecked(pos)`, + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: `pos < end || charCodeUnchecked(pos)`, + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: `pos - 1 < end && charCodeUnchecked(pos)`, + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: `pos < end + 1 && charCodeUnchecked(pos)`, + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: "pos >= end ? charCodeUnchecked(pos) : null", + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: "pos < end ? null : charCodeUnchecked(pos)", + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: "if (pos >= end) charCodeUnchecked(pos)", + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: "if (pos >= end) {charCodeUnchecked(pos)}", + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: "if (pos < end); else charCodeUnchecked(pos)", + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: "if (pos < end); else {charCodeUnchecked(pos)}", + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: "while (pos >= end) charCodeUnchecked(pos)", + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: "while (pos >= end) {charCodeUnchecked(pos)}", + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: "pos < end && (pos++ || charCodeUnchecked(pos))", + errors: [{ messageId: "charCodeUncheckedError" }], + }, + { + filename: "scanner.ts", + code: "function createScanner() { text.charCodeAt(pos) }", + errors: [{ messageId: "charCodeAtError" }], + }, + { + filename: "scanner.ts", + code: String.raw` + if ( + isRegularExpression && shouldEmitInvalidEscapeError && escapedValue >= 0xD800 && escapedValue <= 0xDBFF && + pos + 6 < end && text.substring(pos, pos + 2) === "\\u" && charCodeUnchecked(pos + 2) !== CharacterCodes.openBrace + ) {} + `, + errors: [{ messageId: "charCodeUncheckedError" }], + }, + ], +}); diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 7b7e8875c6711..ec1ea66adca3e 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -1106,7 +1106,8 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean * function does not perform bounds checks. */ function codePointUnchecked(pos: number) { - return codePointAt(text, pos); + // eslint-disable-next-line local/bounds-check -- intentionally unsafe call + return text.codePointAt(pos)!; // Using `!` as we assume you have already checked bounds before calling. } /** @@ -1123,6 +1124,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean * function does not perform bounds checks. */ function charCodeUnchecked(pos: number) { + // eslint-disable-next-line local/bounds-check -- intentionally unsafe call return text.charCodeAt(pos); } @@ -1151,7 +1153,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean let isPreviousTokenSeparator = false; let result = ""; while (true) { - const ch = charCodeUnchecked(pos); + const ch = charCodeChecked(pos); if (ch === CharacterCodes._) { tokenFlags |= TokenFlags.ContainsSeparator; if (allowSeparator) { @@ -1172,15 +1174,14 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean start = pos; continue; } - if (isDigit(ch)) { - allowSeparator = true; - isPreviousTokenSeparator = false; - pos++; - continue; + if (!isDigit(ch)) { + break; } - break; + allowSeparator = true; + isPreviousTokenSeparator = false; + pos++; } - if (charCodeUnchecked(pos - 1) === CharacterCodes._) { + if (charCodeChecked(pos - 1) === CharacterCodes._) { tokenFlags |= TokenFlags.ContainsInvalidSeparator; error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); } @@ -1210,9 +1211,10 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean function scanNumber(): SyntaxKind { let start = pos; let mainFragment: string; + // eslint-disable-next-line local/bounds-check -- all call sites should perform bounds checks if (charCodeUnchecked(pos) === CharacterCodes._0) { pos++; - if (charCodeUnchecked(pos) === CharacterCodes._) { + if (charCodeChecked(pos) === CharacterCodes._) { tokenFlags |= TokenFlags.ContainsSeparator | TokenFlags.ContainsInvalidSeparator; error(Diagnostics.Numeric_separators_are_not_allowed_here, pos, 1); // treat it as a normal number literal @@ -1246,15 +1248,17 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } let decimalFragment: string | undefined; let scientificFragment: string | undefined; - if (charCodeUnchecked(pos) === CharacterCodes.dot) { + if (charCodeChecked(pos) === CharacterCodes.dot) { pos++; decimalFragment = scanNumberFragment(); } let end = pos; - if (charCodeUnchecked(pos) === CharacterCodes.E || charCodeUnchecked(pos) === CharacterCodes.e) { + let ch = charCodeChecked(pos); + if (ch === CharacterCodes.E || ch === CharacterCodes.e) { pos++; tokenFlags |= TokenFlags.Scientific; - if (charCodeUnchecked(pos) === CharacterCodes.plus || charCodeUnchecked(pos) === CharacterCodes.minus) pos++; + ch = charCodeChecked(pos); + if (ch === CharacterCodes.plus || ch === CharacterCodes.minus) pos++; const preNumericPart = pos; const finalFragment = scanNumberFragment(); if (!finalFragment) { @@ -1301,7 +1305,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } function checkForIdentifierStartAfterNumericLiteral(numericStart: number, isScientific?: boolean) { - if (!isIdentifierStart(codePointUnchecked(pos), languageVersion)) { + if (!isIdentifierStart(codePointChecked(pos), languageVersion)) { return; } @@ -1325,10 +1329,10 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean function scanDigits(): boolean { const start = pos; let isOctal = true; - while (isDigit(charCodeChecked(pos))) { - if (!isOctalDigit(charCodeUnchecked(pos))) { - isOctal = false; - } + while (true) { + const ch = charCodeChecked(pos); + if (!isDigit(ch)) break; + isOctal &&= isOctalDigit(ch); pos++; } tokenValue = text.substring(start, pos); @@ -1357,7 +1361,8 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean let allowSeparator = false; let isPreviousTokenSeparator = false; while (valueChars.length < minCount || scanAsManyAsPossible) { - let ch = charCodeUnchecked(pos); + let ch = charCodeChecked(pos); + if (ch === CharacterCodes.EOF) break; if (canHaveSeparators && ch === CharacterCodes._) { tokenFlags |= TokenFlags.ContainsSeparator; if (allowSeparator) { @@ -1390,25 +1395,26 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean if (valueChars.length < minCount) { valueChars = []; } - if (charCodeUnchecked(pos - 1) === CharacterCodes._) { + if (charCodeChecked(pos - 1) === CharacterCodes._) { error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); } return String.fromCharCode(...valueChars); } function scanString(jsxAttributeString = false): string { + // eslint-disable-next-line -- all callers should perform bounds check const quote = charCodeUnchecked(pos); pos++; let result = ""; let start = pos; while (true) { - if (pos >= end) { + const ch = charCodeChecked(pos); + if (ch === CharacterCodes.EOF) { result += text.substring(start, pos); tokenFlags |= TokenFlags.Unterminated; error(Diagnostics.Unterminated_string_literal); break; } - const ch = charCodeUnchecked(pos); if (ch === quote) { result += text.substring(start, pos); pos++; @@ -1437,6 +1443,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean * a literal component of a TemplateExpression. */ function scanTemplateAndSetTokenValue(shouldEmitInvalidEscapeError: boolean): SyntaxKind { + // eslint-disable-next-line local/bounds-check -- all callers should have performed bounds check const startedWithBacktick = charCodeUnchecked(pos) === CharacterCodes.backtick; pos++; @@ -1445,7 +1452,9 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean let resultingToken: SyntaxKind; while (true) { - if (pos >= end) { + const ch = charCodeChecked(pos); + + if (ch === CharacterCodes.EOF) { contents += text.substring(start, pos); tokenFlags |= TokenFlags.Unterminated; error(Diagnostics.Unterminated_template_literal); @@ -1453,10 +1462,8 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean break; } - const currChar = charCodeUnchecked(pos); - // '`' - if (currChar === CharacterCodes.backtick) { + if (ch === CharacterCodes.backtick) { contents += text.substring(start, pos); pos++; resultingToken = startedWithBacktick ? SyntaxKind.NoSubstitutionTemplateLiteral : SyntaxKind.TemplateTail; @@ -1464,7 +1471,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } // '${' - if (currChar === CharacterCodes.$ && pos + 1 < end && charCodeUnchecked(pos + 1) === CharacterCodes.openBrace) { + if (ch === CharacterCodes.$ && charCodeChecked(pos + 1) === CharacterCodes.openBrace) { contents += text.substring(start, pos); pos += 2; resultingToken = startedWithBacktick ? SyntaxKind.TemplateHead : SyntaxKind.TemplateMiddle; @@ -1472,7 +1479,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } // Escape character - if (currChar === CharacterCodes.backslash) { + if (ch === CharacterCodes.backslash) { contents += text.substring(start, pos); contents += scanEscapeSequence(shouldEmitInvalidEscapeError, /*isRegularExpression*/ false); start = pos; @@ -1481,11 +1488,11 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean // Speculated ECMAScript 6 Spec 11.8.6.1: // and LineTerminatorSequences are normalized to for Template Values - if (currChar === CharacterCodes.carriageReturn) { + if (ch === CharacterCodes.carriageReturn) { contents += text.substring(start, pos); pos++; - if (pos < end && charCodeUnchecked(pos) === CharacterCodes.lineFeed) { + if (charCodeChecked(pos) === CharacterCodes.lineFeed) { pos++; } @@ -1520,17 +1527,17 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean function scanEscapeSequence(shouldEmitInvalidEscapeError: boolean, isRegularExpression: boolean | "annex-b"): string { const start = pos; pos++; - if (pos >= end) { + const ch = charCodeChecked(pos); + if (ch === CharacterCodes.EOF) { error(Diagnostics.Unexpected_end_of_text); return ""; } - const ch = charCodeUnchecked(pos); pos++; switch (ch) { case CharacterCodes._0: // Although '0' preceding any digit is treated as LegacyOctalEscapeSequence, // '\08' should separately be interpreted as '\0' + '8'. - if (pos >= end || !isDigit(charCodeUnchecked(pos))) { + if (!isDigit(charCodeChecked(pos))) { return "\0"; } // '\01', '\011' @@ -1539,7 +1546,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean case CharacterCodes._2: case CharacterCodes._3: // '\1', '\17', '\177' - if (pos < end && isOctalDigit(charCodeUnchecked(pos))) { + if (isOctalDigit(charCodeChecked(pos))) { pos++; } // '\17', '\177' @@ -1549,7 +1556,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean case CharacterCodes._6: case CharacterCodes._7: // '\4', '\47' but not '\477' - if (pos < end && isOctalDigit(charCodeUnchecked(pos))) { + if (isOctalDigit(charCodeChecked(pos))) { pos++; } // '\47' @@ -1590,7 +1597,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean case CharacterCodes.u: if ( (!isRegularExpression || shouldEmitInvalidEscapeError) && - pos < end && charCodeUnchecked(pos) === CharacterCodes.openBrace + charCodeChecked(pos) === CharacterCodes.openBrace ) { // '\u{DDDDDD}' pos -= 2; @@ -1598,7 +1605,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } // '\uDDDD' for (; pos < start + 6; pos++) { - if (!(pos < end && isHexDigit(charCodeUnchecked(pos)))) { + if (!isHexDigit(charCodeChecked(pos))) { tokenFlags |= TokenFlags.ContainsInvalidEscape; if (isRegularExpression || shouldEmitInvalidEscapeError) { error(Diagnostics.Hexadecimal_digit_expected); @@ -1611,6 +1618,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean const escapedValueString = String.fromCharCode(escapedValue); if ( isRegularExpression && shouldEmitInvalidEscapeError && escapedValue >= 0xD800 && escapedValue <= 0xDBFF && + // eslint-disable-next-line local/bounds-check -- bounds checked before invocation pos + 6 < end && text.substring(pos, pos + 2) === "\\u" && charCodeUnchecked(pos + 2) !== CharacterCodes.openBrace ) { // For regular expressions in Unicode mode, \u HexLeadSurrogate \u HexTrailSurrogate is treated as a single character @@ -1619,6 +1627,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean const nextStart = pos; let nextPos = pos + 2; for (; nextPos < nextStart + 6; nextPos++) { + // eslint-disable-next-line local/bounds-check -- bounds checked before invocation if (!isHexDigit(charCodeUnchecked(pos))) { // leave the error to the next call return escapedValueString; @@ -1727,7 +1736,11 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } function peekExtendedUnicodeEscape(): number { - if (codePointUnchecked(pos + 1) === CharacterCodes.u && codePointUnchecked(pos + 2) === CharacterCodes.openBrace) { + if ( + pos + 2 < end && + codePointUnchecked(pos + 1) === CharacterCodes.u && + codePointUnchecked(pos + 2) === CharacterCodes.openBrace + ) { const start = pos; pos += 3; const escapedValueString = scanMinimumNumberOfHexDigits(1, /*canHaveSeparators*/ false); @@ -1794,7 +1807,10 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean let separatorAllowed = false; let isPreviousTokenSeparator = false; while (true) { - const ch = charCodeUnchecked(pos); + const ch = charCodeChecked(pos); + if (ch === CharacterCodes.EOF) { + break; + } // Numeric separators are allowed anywhere within a numeric literal, except not at the beginning, or following another separator if (ch === CharacterCodes._) { tokenFlags |= TokenFlags.ContainsSeparator; @@ -1819,7 +1835,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean pos++; isPreviousTokenSeparator = false; } - if (charCodeUnchecked(pos - 1) === CharacterCodes._) { + if (charCodeChecked(pos - 1) === CharacterCodes._) { // Literal ends with underscore - not allowed error(Diagnostics.Numeric_separators_are_not_allowed_here, pos - 1, 1); } @@ -1827,7 +1843,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } function checkBigIntSuffix(): SyntaxKind { - if (charCodeUnchecked(pos) === CharacterCodes.n) { + if (charCodeChecked(pos) === CharacterCodes.n) { tokenValue += "n"; // Use base 10 instead of base 2 or base 8 for shorter literals if (tokenFlags & TokenFlags.BinaryOrOctalSpecifier) { @@ -1854,11 +1870,12 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean let asteriskSeen = false; while (true) { tokenStart = pos; - if (pos >= end) { + + const ch = codePointChecked(pos); + if (ch === CharacterCodes.EOF) { return token = SyntaxKind.EndOfFileToken; } - const ch = codePointUnchecked(pos); if (pos === 0) { // Special handling for shebang if (ch === CharacterCodes.hash && isShebangTrivia(text, pos)) { @@ -1923,8 +1940,8 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean return token = SyntaxKind.WhitespaceTrivia; } case CharacterCodes.exclamation: - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { - if (charCodeUnchecked(pos + 2) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 2) === CharacterCodes.equals) { return pos += 3, token = SyntaxKind.ExclamationEqualsEqualsToken; } return pos += 2, token = SyntaxKind.ExclamationEqualsToken; @@ -1938,19 +1955,19 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean case CharacterCodes.backtick: return token = scanTemplateAndSetTokenValue(/*shouldEmitInvalidEscapeError*/ false); case CharacterCodes.percent: - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.PercentEqualsToken; } pos++; return token = SyntaxKind.PercentToken; case CharacterCodes.ampersand: - if (charCodeUnchecked(pos + 1) === CharacterCodes.ampersand) { - if (charCodeUnchecked(pos + 2) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.ampersand) { + if (charCodeChecked(pos + 2) === CharacterCodes.equals) { return pos += 3, token = SyntaxKind.AmpersandAmpersandEqualsToken; } return pos += 2, token = SyntaxKind.AmpersandAmpersandToken; } - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.AmpersandEqualsToken; } pos++; @@ -1962,11 +1979,11 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean pos++; return token = SyntaxKind.CloseParenToken; case CharacterCodes.asterisk: - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.AsteriskEqualsToken; } - if (charCodeUnchecked(pos + 1) === CharacterCodes.asterisk) { - if (charCodeUnchecked(pos + 2) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.asterisk) { + if (charCodeChecked(pos + 2) === CharacterCodes.equals) { return pos += 3, token = SyntaxKind.AsteriskAsteriskEqualsToken; } return pos += 2, token = SyntaxKind.AsteriskAsteriskToken; @@ -1979,10 +1996,10 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } return token = SyntaxKind.AsteriskToken; case CharacterCodes.plus: - if (charCodeUnchecked(pos + 1) === CharacterCodes.plus) { + if (charCodeChecked(pos + 1) === CharacterCodes.plus) { return pos += 2, token = SyntaxKind.PlusPlusToken; } - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.PlusEqualsToken; } pos++; @@ -1991,27 +2008,27 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean pos++; return token = SyntaxKind.CommaToken; case CharacterCodes.minus: - if (charCodeUnchecked(pos + 1) === CharacterCodes.minus) { + if (charCodeChecked(pos + 1) === CharacterCodes.minus) { return pos += 2, token = SyntaxKind.MinusMinusToken; } - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.MinusEqualsToken; } pos++; return token = SyntaxKind.MinusToken; case CharacterCodes.dot: - if (isDigit(charCodeUnchecked(pos + 1))) { + if (isDigit(charCodeChecked(pos + 1))) { scanNumber(); return token = SyntaxKind.NumericLiteral; } - if (charCodeUnchecked(pos + 1) === CharacterCodes.dot && charCodeUnchecked(pos + 2) === CharacterCodes.dot) { + if (charCodeChecked(pos + 1) === CharacterCodes.dot && charCodeChecked(pos + 2) === CharacterCodes.dot) { return pos += 3, token = SyntaxKind.DotDotDotToken; } pos++; return token = SyntaxKind.DotToken; case CharacterCodes.slash: // Single-line comment - if (charCodeUnchecked(pos + 1) === CharacterCodes.slash) { + if (charCodeChecked(pos + 1) === CharacterCodes.slash) { pos += 2; while (pos < end) { @@ -2036,16 +2053,16 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } } // Multi-line comment - if (charCodeUnchecked(pos + 1) === CharacterCodes.asterisk) { + if (charCodeChecked(pos + 1) === CharacterCodes.asterisk) { pos += 2; - const isJSDoc = charCodeUnchecked(pos) === CharacterCodes.asterisk && charCodeUnchecked(pos + 1) !== CharacterCodes.slash; + const isJSDoc = charCodeChecked(pos) === CharacterCodes.asterisk && charCodeChecked(pos + 1) !== CharacterCodes.slash; let commentClosed = false; let lastLineStart = tokenStart; while (pos < end) { const ch = charCodeUnchecked(pos); - if (ch === CharacterCodes.asterisk && charCodeUnchecked(pos + 1) === CharacterCodes.slash) { + if (ch === CharacterCodes.asterisk && charCodeChecked(pos + 1) === CharacterCodes.slash) { pos += 2; commentClosed = true; break; @@ -2080,7 +2097,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } } - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.SlashEqualsToken; } @@ -2149,19 +2166,19 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } } - if (charCodeUnchecked(pos + 1) === CharacterCodes.lessThan) { - if (charCodeUnchecked(pos + 2) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.lessThan) { + if (charCodeChecked(pos + 2) === CharacterCodes.equals) { return pos += 3, token = SyntaxKind.LessThanLessThanEqualsToken; } return pos += 2, token = SyntaxKind.LessThanLessThanToken; } - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.LessThanEqualsToken; } if ( languageVariant === LanguageVariant.JSX && - charCodeUnchecked(pos + 1) === CharacterCodes.slash && - charCodeUnchecked(pos + 2) !== CharacterCodes.asterisk + charCodeChecked(pos + 1) === CharacterCodes.slash && + charCodeChecked(pos + 2) !== CharacterCodes.asterisk ) { return pos += 2, token = SyntaxKind.LessThanSlashToken; } @@ -2178,13 +2195,13 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } } - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { - if (charCodeUnchecked(pos + 2) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 2) === CharacterCodes.equals) { return pos += 3, token = SyntaxKind.EqualsEqualsEqualsToken; } return pos += 2, token = SyntaxKind.EqualsEqualsToken; } - if (charCodeUnchecked(pos + 1) === CharacterCodes.greaterThan) { + if (charCodeChecked(pos + 1) === CharacterCodes.greaterThan) { return pos += 2, token = SyntaxKind.EqualsGreaterThanToken; } pos++; @@ -2203,11 +2220,11 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean pos++; return token = SyntaxKind.GreaterThanToken; case CharacterCodes.question: - if (charCodeUnchecked(pos + 1) === CharacterCodes.dot && !isDigit(charCodeUnchecked(pos + 2))) { + if (charCodeChecked(pos + 1) === CharacterCodes.dot && !isDigit(charCodeChecked(pos + 2))) { return pos += 2, token = SyntaxKind.QuestionDotToken; } - if (charCodeUnchecked(pos + 1) === CharacterCodes.question) { - if (charCodeUnchecked(pos + 2) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.question) { + if (charCodeChecked(pos + 2) === CharacterCodes.equals) { return pos += 3, token = SyntaxKind.QuestionQuestionEqualsToken; } return pos += 2, token = SyntaxKind.QuestionQuestionToken; @@ -2221,7 +2238,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean pos++; return token = SyntaxKind.CloseBracketToken; case CharacterCodes.caret: - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.CaretEqualsToken; } pos++; @@ -2240,13 +2257,13 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } } - if (charCodeUnchecked(pos + 1) === CharacterCodes.bar) { - if (charCodeUnchecked(pos + 2) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.bar) { + if (charCodeChecked(pos + 2) === CharacterCodes.equals) { return pos += 3, token = SyntaxKind.BarBarEqualsToken; } return pos += 2, token = SyntaxKind.BarBarToken; } - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.BarEqualsToken; } pos++; @@ -2285,7 +2302,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean return token = SyntaxKind.Unknown; } - const charAfterHash = codePointUnchecked(pos + 1); + const charAfterHash = codePointChecked(pos + 1); if (charAfterHash === CharacterCodes.backslash) { pos++; const extendedCookedChar = peekExtendedUnicodeEscape(); @@ -2370,7 +2387,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean Debug.assert(token === SyntaxKind.Unknown, "'reScanInvalidIdentifier' should only be called when the current token is 'SyntaxKind.Unknown'."); pos = tokenStart = fullStartPos; tokenFlags = 0; - const ch = codePointUnchecked(pos); + const ch = codePointChecked(pos); const identifierKind = scanIdentifier(ch, ScriptTarget.ESNext); if (identifierKind) { return token = identifierKind; @@ -2394,20 +2411,20 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean function reScanGreaterToken(): SyntaxKind { if (token === SyntaxKind.GreaterThanToken) { - if (charCodeUnchecked(pos) === CharacterCodes.greaterThan) { - if (charCodeUnchecked(pos + 1) === CharacterCodes.greaterThan) { - if (charCodeUnchecked(pos + 2) === CharacterCodes.equals) { + if (charCodeChecked(pos) === CharacterCodes.greaterThan) { + if (charCodeChecked(pos + 1) === CharacterCodes.greaterThan) { + if (charCodeChecked(pos + 2) === CharacterCodes.equals) { return pos += 3, token = SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken; } return pos += 2, token = SyntaxKind.GreaterThanGreaterThanGreaterThanToken; } - if (charCodeUnchecked(pos + 1) === CharacterCodes.equals) { + if (charCodeChecked(pos + 1) === CharacterCodes.equals) { return pos += 2, token = SyntaxKind.GreaterThanGreaterThanEqualsToken; } pos++; return token = SyntaxKind.GreaterThanGreaterThanToken; } - if (charCodeUnchecked(pos) === CharacterCodes.equals) { + if (charCodeChecked(pos) === CharacterCodes.equals) { pos++; return token = SyntaxKind.GreaterThanEqualsToken; } @@ -2436,13 +2453,13 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean while (true) { // If we reach the end of a file, or hit a newline, then this is an unterminated // regex. Report error and return what we have so far. - if (p >= end) { + const ch = charCodeChecked(p); + if (ch === CharacterCodes.EOF) { tokenFlags |= TokenFlags.Unterminated; error(Diagnostics.Unterminated_regular_expression_literal); break; } - const ch = charCodeUnchecked(p); if (isLineBreak(ch)) { tokenFlags |= TokenFlags.Unterminated; error(Diagnostics.Unterminated_regular_expression_literal); @@ -2792,7 +2809,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean // | CharacterEscape // | 'k<' RegExpIdentifierName '>' function scanAtomEscape() { - Debug.assertEqual(charCodeUnchecked(pos - 1), CharacterCodes.backslash); + Debug.assertEqual(charCodeChecked(pos - 1), CharacterCodes.backslash); switch (charCodeChecked(pos)) { case CharacterCodes.k: pos++; @@ -2822,7 +2839,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean // DecimalEscape ::= [1-9] [0-9]* function scanDecimalEscape(): boolean { - Debug.assertEqual(charCodeUnchecked(pos - 1), CharacterCodes.backslash); + Debug.assertEqual(charCodeChecked(pos - 1), CharacterCodes.backslash); const ch = charCodeChecked(pos); if (ch >= CharacterCodes._1 && ch <= CharacterCodes._9) { const start = pos; @@ -2841,7 +2858,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean // | '^' | '$' | '/' | '\' | '.' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' // | [~UnicodeMode] (any other non-identifier characters) function scanCharacterEscape(atomEscape: boolean): string { - Debug.assertEqual(charCodeUnchecked(pos - 1), CharacterCodes.backslash); + Debug.assertEqual(charCodeChecked(pos - 1), CharacterCodes.backslash); let ch = charCodeChecked(pos); switch (ch) { case CharacterCodes.c: @@ -2892,7 +2909,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean } function scanGroupName(isReference: boolean) { - Debug.assertEqual(charCodeUnchecked(pos - 1), CharacterCodes.lessThan); + Debug.assertEqual(charCodeChecked(pos - 1), CharacterCodes.lessThan); tokenStart = pos; scanIdentifier(codePointChecked(pos), languageVersion); if (pos === tokenStart) { @@ -2918,7 +2935,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean // ClassRanges ::= '^'? (ClassAtom ('-' ClassAtom)?)* function scanClassRanges() { - Debug.assertEqual(charCodeUnchecked(pos - 1), CharacterCodes.openBracket); + Debug.assertEqual(charCodeChecked(pos - 1), CharacterCodes.openBracket); if (charCodeChecked(pos) === CharacterCodes.caret) { // character complement pos++; @@ -2977,7 +2994,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean // ClassSubtraction ::= ClassSetOperand ('--' ClassSetOperand)+ // ClassSetRange ::= ClassSetCharacter '-' ClassSetCharacter function scanClassSetExpression() { - Debug.assertEqual(charCodeUnchecked(pos - 1), CharacterCodes.openBracket); + Debug.assertEqual(charCodeChecked(pos - 1), CharacterCodes.openBracket); let isCharacterComplement = false; if (charCodeChecked(pos) === CharacterCodes.caret) { pos++; @@ -3221,7 +3238,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean // ClassStringDisjunctionContents ::= ClassSetCharacter* ('|' ClassSetCharacter*)* function scanClassStringDisjunctionContents() { - Debug.assertEqual(charCodeUnchecked(pos - 1), CharacterCodes.openBrace); + Debug.assertEqual(charCodeChecked(pos - 1), CharacterCodes.openBrace); let characterCount = 0; while (true) { const ch = charCodeChecked(pos); @@ -3361,7 +3378,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean // | 'd' | 'D' | 's' | 'S' | 'w' | 'W' // | [+UnicodeMode] ('P' | 'p') '{' UnicodePropertyValueExpression '}' function scanCharacterClassEscape(): boolean { - Debug.assertEqual(charCodeUnchecked(pos - 1), CharacterCodes.backslash); + Debug.assertEqual(charCodeChecked(pos - 1), CharacterCodes.backslash); let isCharacterComplement = false; const start = pos - 1; const ch = charCodeChecked(pos); @@ -3581,13 +3598,13 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean function scanJsxToken(allowMultilineJsxText = true): JsxTokenSyntaxKind { fullStartPos = tokenStart = pos; - if (pos >= end) { + let char = charCodeChecked(pos); + if (char === CharacterCodes.EOF) { return token = SyntaxKind.EndOfFileToken; } - let char = charCodeUnchecked(pos); if (char === CharacterCodes.lessThan) { - if (charCodeUnchecked(pos + 1) === CharacterCodes.slash) { + if (charCodeChecked(pos + 1) === CharacterCodes.slash) { pos += 2; return token = SyntaxKind.LessThanSlashToken; } @@ -3680,7 +3697,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean function scanJsxAttributeValue(): SyntaxKind { fullStartPos = pos; - switch (charCodeUnchecked(pos)) { + switch (charCodeChecked(pos)) { case CharacterCodes.doubleQuote: case CharacterCodes.singleQuote: tokenValue = scanString(/*jsxAttributeString*/ true); @@ -3702,13 +3719,14 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean if (pos >= end) { return token = SyntaxKind.EndOfFileToken; } - for (let ch = charCodeUnchecked(pos); pos < end && (!isLineBreak(ch) && ch !== CharacterCodes.backtick); ch = codePointUnchecked(++pos)) { + for (let ch = charCodeChecked(pos); ch !== CharacterCodes.EOF && (!isLineBreak(ch) && ch !== CharacterCodes.backtick); ch = codePointChecked(++pos)) { if (!inBackticks) { if (ch === CharacterCodes.openBrace) { break; } else if ( ch === CharacterCodes.at + // eslint-disable-next-line local/bounds-check -- bounds checked before invocation && pos - 1 >= 0 && isWhiteSpaceSingleLine(charCodeUnchecked(pos - 1)) && !(pos + 1 < end && isWhiteSpaceLike(charCodeUnchecked(pos + 1))) ) { @@ -3727,11 +3745,12 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean function scanJsDocToken(): JSDocSyntaxKind { fullStartPos = tokenStart = pos; tokenFlags = TokenFlags.None; - if (pos >= end) { + + const ch = codePointChecked(pos); + if (ch === CharacterCodes.EOF) { return token = SyntaxKind.EndOfFileToken; } - const ch = codePointUnchecked(pos); pos += charSize(ch); switch (ch) { case CharacterCodes.tab: @@ -3745,7 +3764,7 @@ export function createScanner(languageVersion: ScriptTarget, skipTrivia: boolean case CharacterCodes.at: return token = SyntaxKind.AtToken; case CharacterCodes.carriageReturn: - if (charCodeUnchecked(pos) === CharacterCodes.lineFeed) { + if (charCodeChecked(pos) === CharacterCodes.lineFeed) { pos++; } // falls through