From dca86333ba11df0e5d60d9badbbb42afbaa053c8 Mon Sep 17 00:00:00 2001 From: JounQin Date: Fri, 2 Aug 2019 17:59:54 +0800 Subject: [PATCH] feat: add new rule no-unescaped-entities --- .eslintrc.js | 1 + .travis.yml | 4 ++ package.json | 3 +- src/helper.ts | 70 ++------------------ src/index.ts | 2 + src/normalizer.ts | 75 ++++++++++++++++++---- src/parser.ts | 12 ++-- src/regexp.ts | 4 +- src/rules/index.ts | 2 + src/rules/no-jsx-html-comments.ts | 47 +++++++------- src/rules/no-unescaped-entities.ts | 100 +++++++++++++++++++++++++++++ src/rules/types.ts | 12 ++-- src/traverse.ts | 68 +++++++++++++++++++- test/fixture1.mdx | 28 +------- types.d.ts | 6 ++ yarn.lock | 36 ++++++++--- 16 files changed, 320 insertions(+), 150 deletions(-) create mode 100644 src/rules/no-unescaped-entities.ts diff --git a/.eslintrc.js b/.eslintrc.js index 14ba541d..41e4ee0d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -22,6 +22,7 @@ module.exports = { files: ['*.d.ts'], rules: { 'import/order': 0, + 'import/no-duplicates': 0, 'import/no-unresolved': 0, }, }, diff --git a/.travis.yml b/.travis.yml index 9653ff8f..02b900f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,10 @@ node_js: --lts cache: yarn +branches: + except: + - develop + before_install: - curl -o- -L https://yarnpkg.com/install.sh | bash - export PATH="$HOME/.yarn/bin:$PATH" diff --git a/package.json b/package.json index cad48926..de0a3298 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "prepublishOnly": "yarn build", "build": "tsc -P src", "test": "jest", - "lint": "eslint . --ext js,mdx,ts,tsx" + "lint": "EFF_NO_LINK_RULES=true eslint . --ext js,mdx,ts,tsx -f friendly" }, "keywords": [ "eslint", @@ -45,6 +45,7 @@ "commitlint": "^8.1.0", "eslint": "^6.1.0", "eslint-config-1stg": "~5.4.1", + "eslint-formatter-friendly": "^7.0.0", "eslint-plugin-jest": "^22.14.1", "husky": "^3.0.2", "jest": "^24.8.0", diff --git a/src/helper.ts b/src/helper.ts index 836ce1c8..5c05d090 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,14 +1,6 @@ -import { - isOpenTag, - isCloseTag, - isComment, - isSelfClosingTag, - isOpenCloseTag, -} from './regexp' - -import { Node, Position } from 'unist' +import { Position } from 'unist' import { AST } from 'eslint' -// SourceLocation` is not exported from estree, but it is actually working +// `SourceLocation` is not exported from estree, but it is actually working // eslint-disable-next-line import/named import { SourceLocation } from 'estree' @@ -82,59 +74,7 @@ export function restoreNodeLocation( } } -// fix #7 -export const combineJsxNodes = (nodes: Node[]) => { - let offset = 0 - const jsxNodes: Node[] = [] - const { length } = nodes - return nodes.reduce((acc, node, index) => { - if (node.type === 'jsx') { - const rawText = node.value as string - if (isOpenTag(rawText as string)) { - offset++ - jsxNodes.push(node) - } else { - if (isCloseTag(rawText)) { - offset-- - } else if ( - !isComment(rawText) && - !isSelfClosingTag(rawText) && - !isOpenCloseTag(rawText) - ) { - const { start } = node.position - throw Object.assign( - new SyntaxError( - `'Unknown node type: ${JSON.stringify( - node.type, - )}, text: ${JSON.stringify(rawText)}`, - ), - { - lineNumber: start.line, - column: start.column, - index: start.offset, - }, - ) - } - - jsxNodes.push(node) +export const first = (items: T[] | ReadonlyArray) => items && items[0] - if (!offset || index === length - 1) { - acc.push({ - type: 'jsx', - value: jsxNodes.reduce((acc, { value }) => (acc += value), ''), - position: { - start: jsxNodes[0].position.start, - end: jsxNodes[jsxNodes.length - 1].position.end, - }, - }) - jsxNodes.length = 0 - } - } - } else if (offset) { - jsxNodes.push(node) - } else { - acc.push(node) - } - return acc - }, []) -} +export const last = (items: T[] | ReadonlyArray) => + items && items[items.length - 1] diff --git a/src/index.ts b/src/index.ts index c381c787..51baad15 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,8 +13,10 @@ export const configs = { plugins: ['@rxts/mdx'], rules: { '@rxts/mdx/no-jsx-html-comments': 2, + '@rxts/mdx/no-unescaped-entities': 2, '@rxts/mdx/no-unused-expressions': 2, 'no-unused-expressions': 0, + 'react/no-unescaped-entities': 0, 'react/react-in-jsx-scope': 0, }, }, diff --git a/src/normalizer.ts b/src/normalizer.ts index 1a30ce40..958c3c2c 100644 --- a/src/normalizer.ts +++ b/src/normalizer.ts @@ -1,27 +1,76 @@ +import { last } from './helper' import { isComment, COMMENT_CONTENT_REGEX } from './regexp' -import { Node } from 'unist' +import { Node, Point, Parent } from 'unist' -export const normalizeJsxNode = (node: Node) => { - let rawText = node.value as string +export interface Comment { + fixed: string + loc: { + start: Point + end: Point + } + origin: string +} + +export const normalizeJsxNode = (node: Node, parent?: Parent) => { + const value = node.value as string - if (isComment(rawText)) { + if (isComment(value) || !parent) { return node } - const matched = rawText.match(COMMENT_CONTENT_REGEX) + const matched = value.match(COMMENT_CONTENT_REGEX) if (!matched) { return node } - node.jsxType = 'JSXElementWithHTMLComments' - node.raw = rawText - rawText = node.value = rawText.replace( - COMMENT_CONTENT_REGEX, - (_matched, $0, $1, $2) => - `{/${'*'.repeat($0.length - 1)}${$1}${'*'.repeat($2.length - 1)}/}`, - ) + const comments: Comment[] = [] + const { + position: { + start: { line, column, offset: startOffset }, + }, + } = node - return node + return Object.assign(node, { + data: { + ...node.data, + jsxType: 'JSXElementWithHTMLComments', + comments, + // jsx in paragraph is considered as plain html in mdx, what means html style comments are valid + // TODO: in this case, jsx style comments could be a mistake + inline: parent.type !== 'root', + }, + value: value.replace( + COMMENT_CONTENT_REGEX, + (matched: string, $0: string, $1: string, $2: string, offset: number) => { + const endOffset = offset + matched.length + const startLines = value.slice(0, offset).split('\n') + const endLines = value.slice(0, endOffset).split('\n') + const fixed = `{/${'*'.repeat($0.length - 2)}${$1}${'*'.repeat( + $2.length - 2, + )}/}` + const startLineOffset = startLines.length - 1 + const endLineOffset = endLines.length - 1 + comments.push({ + fixed, + loc: { + start: { + line: line + startLineOffset, + column: + last(startLines).length + (startLineOffset ? 0 : column - 1), + offset: startOffset + offset, + }, + end: { + line: line + endLineOffset - 1, + column: last(endLines).length + (endLineOffset ? 0 : column - 1), + offset: startOffset + endOffset, + }, + }, + origin: matched, + }) + return fixed + }, + ), + }) } diff --git a/src/parser.ts b/src/parser.ts index 432c1ca3..ced659f9 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -70,21 +70,21 @@ export const parseForESLint = ( } traverse(root, { - enter(node) { + enter(node, parent) { if (!ES_NODE_TYPES.includes(node.type as EsNodeType)) { return } - normalizeJsxNode(node) + normalizeJsxNode(node, parent) - if (node.jsxType === 'JSXElementWithHTMLComments') { + if (node.data && node.data.jsxType === 'JSXElementWithHTMLComments') { services.JSXElementsWithHTMLComments.push(node) } - const rawText = node.value as string + const value = node.value as string // fix #4 - if (isComment(rawText)) { + if (isComment(value)) { return } @@ -94,7 +94,7 @@ export const parseForESLint = ( let program: AST.Program | Linter.ESLintParseResult try { - program = parser(rawText, options) + program = parser(value, options) } catch (e) { if (e instanceof SyntaxError) { e.index += start diff --git a/src/regexp.ts b/src/regexp.ts index 7801a7a4..83e3db92 100644 --- a/src/regexp.ts +++ b/src/regexp.ts @@ -23,8 +23,8 @@ const openTag = '<[A-Za-z]*[A-Za-z0-9\\.\\-]*' + attribute + '*\\s*>' const closeTag = '<\\s*\\/[A-Za-z]*[A-Za-z0-9\\.\\-]*\\s*>' const selfClosingTag = '<[A-Za-z]*[A-Za-z0-9\\.\\-]*' + attribute + '*\\s*\\/?>' const comment = '|' -const commentOpen = '<(!---*)' -const commentClose = '(-*--)>' +const commentOpen = '()' export const OPEN_TAG_REGEX = new RegExp(`^(?:${openTag})$`) export const CLOSE_TAG_REGEX = new RegExp(`^(?:${closeTag})$`) diff --git a/src/rules/index.ts b/src/rules/index.ts index 83da392d..142e332d 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -1,9 +1,11 @@ import { noJsxHtmlComments } from './no-jsx-html-comments' +import { noUnEscapedEntities } from './no-unescaped-entities' import { noUnUsedExpressions } from './no-unused-expressions' export { noJsxHtmlComments, noUnUsedExpressions } export const rules = { 'no-jsx-html-comments': noJsxHtmlComments, + 'no-unescaped-entities': noUnEscapedEntities, 'no-unused-expressions': noUnUsedExpressions, } diff --git a/src/rules/no-jsx-html-comments.ts b/src/rules/no-jsx-html-comments.ts index b91989f8..01295f07 100644 --- a/src/rules/no-jsx-html-comments.ts +++ b/src/rules/no-jsx-html-comments.ts @@ -1,3 +1,5 @@ +import { Comment } from '../normalizer' + import { ExpressionStatementWithParent, JSX_TYPES, JsxType } from './types' import { Rule } from 'eslint' @@ -12,7 +14,7 @@ export const noJsxHtmlComments: Rule.RuleModule = { recommended: true, }, messages: { - jdxHtmlComments: 'html style comments are invalid in jsx: {{ raw }}', + jsxHtmlComments: 'html style comments are invalid in jsx: {{ origin }}', }, fixable: 'code', schema: [], @@ -33,30 +35,29 @@ export const noJsxHtmlComments: Rule.RuleModule = { } const invalidNode = invalidNodes.shift() - // unist column is 1-indexed, but estree is 0-indexed... - const { start, end } = invalidNode.position - context.report({ - messageId: 'jdxHtmlComments', - data: { - raw: invalidNode.raw as string, - }, - loc: { - start: { - ...start, - column: start.column - 1, + + if (invalidNode.data.inline) { + return + } + + const comments = invalidNode.data.comments as Comment[] + + comments.forEach(({ fixed, loc, origin }) => + context.report({ + messageId: 'jsxHtmlComments', + data: { + origin, }, - end: { - ...end, - column: end.column - 1, + loc, + node, + fix(fixer) { + return fixer.replaceTextRange( + [loc.start.offset, loc.end.offset], + fixed, + ) }, - }, - fix(fixer) { - return fixer.replaceTextRange( - [start.offset, end.offset], - invalidNode.value as string, - ) - }, - }) + }), + ) }, } }, diff --git a/src/rules/no-unescaped-entities.ts b/src/rules/no-unescaped-entities.ts new file mode 100644 index 00000000..a6420ff6 --- /dev/null +++ b/src/rules/no-unescaped-entities.ts @@ -0,0 +1,100 @@ +// eslint-disable-next-line @typescript-eslint/no-triple-slash-reference +/// + +import reactNoUnEscapedEntities from 'eslint-plugin-react/lib/rules/no-unescaped-entities' + +import { first } from '../helper' + +import { NodeWithParent } from './types' + +import { Rule, SourceCode } from 'eslint' + +export type EscapeEntity = + | string + | { + char: string + alternatives: string[] + } + +// copied from `eslint-plugin-react` +const DEFAULTS: EscapeEntity[] = [ + { + char: '>', + alternatives: ['>'], + }, + { + char: '"', + alternatives: ['"', '“', '"', '”'], + }, + { + char: "'", + alternatives: [''', '‘', ''', '’'], + }, + { + char: '}', + alternatives: ['}'], + }, +] + +const EXPRESSION = 'Literal, JSXText' + +export const noUnEscapedEntities: Rule.RuleModule = { + ...reactNoUnEscapedEntities, + create(context) { + const esLintRuleListener = reactNoUnEscapedEntities.create(context) + const configuration = context.options[0] || {} + const entities: EscapeEntity[] = configuration.forbid || DEFAULTS + return { + [EXPRESSION](node: NodeWithParent) { + let { parent } = node + while (parent) { + if (parent.parent.type === 'Program') { + break + } else { + parent = parent.parent + } + } + if (parent.type === 'ExpressionStatement') { + const sourceCode = context.getSourceCode() + const firstLine = sourceCode.getLines()[parent.loc.start.line - 1] + const jsxFirstLine = first( + SourceCode.splitLines(sourceCode.getText(parent)), + ) + const firstLineOffset = firstLine.length - jsxFirstLine.length + const nodeText = sourceCode.getText(node) + const line = 1 + const column = 1 + console.log(nodeText) + entities.forEach(entity => { + let index: number + if (typeof entity === 'string') { + if ((index = nodeText.indexOf(entity)) !== -1) { + context.report({ + loc: { line, column }, + message: `HTML entity, \`${entity}\` , must be escaped.`, + node, + }) + } + } else if ((index = nodeText.indexOf(entity.char)) !== -1) { + context.report({ + loc: { line, column }, + message: `\`${ + entity.char + }\` can be escaped with ${entity.alternatives + .map(alt => `\`${alt}\``) + .join(', ')}.`, + node, + }) + } + }) + // inline html comments should not be escaped + if (firstLineOffset && firstLine.endsWith(jsxFirstLine)) { + return + } + } + // @ts-ignore + esLintRuleListener[EXPRESSION](node) + }, + } + }, +} diff --git a/src/rules/types.ts b/src/rules/types.ts index eae84ea9..a7c25a93 100644 --- a/src/rules/types.ts +++ b/src/rules/types.ts @@ -4,8 +4,12 @@ export const JSX_TYPES = ['JSXElement', 'JSXFragment'] as const export type JsxType = (typeof JSX_TYPES)[number] -export interface ExpressionStatementWithParent extends ExpressionStatement { - parent?: { - type: Node['type'] - } +export interface WithParent { + parent?: NodeWithParent } + +export type NodeWithParent = Node & WithParent + +export interface ExpressionStatementWithParent + extends ExpressionStatement, + WithParent {} diff --git a/src/traverse.ts b/src/traverse.ts index 69c926e3..5322ec35 100644 --- a/src/traverse.ts +++ b/src/traverse.ts @@ -1,4 +1,11 @@ -import { combineJsxNodes } from './helper' +import { last } from './helper' +import { + isOpenTag, + isCloseTag, + isComment, + isSelfClosingTag, + isOpenCloseTag, +} from './regexp' import { Node, Parent } from 'unist' @@ -15,6 +22,63 @@ export class Traverse { this._enter = enter } + // fix #7 + combineJsxNodes(nodes: Node[]) { + let offset = 0 + const jsxNodes: Node[] = [] + const { length } = nodes + return nodes.reduce((acc, node, index) => { + if (node.type === 'jsx') { + const rawText = node.value as string + if (isOpenTag(rawText as string)) { + offset++ + jsxNodes.push(node) + } else { + if (isCloseTag(rawText)) { + offset-- + } else if ( + !isComment(rawText) && + !isSelfClosingTag(rawText) && + !isOpenCloseTag(rawText) + ) { + const { start } = node.position + throw Object.assign( + new SyntaxError( + `'Unknown node type: ${JSON.stringify( + node.type, + )}, text: ${JSON.stringify(rawText)}`, + ), + { + lineNumber: start.line, + column: start.column, + index: start.offset, + }, + ) + } + + jsxNodes.push(node) + + if (!offset || index === length - 1) { + acc.push({ + type: 'jsx', + value: jsxNodes.reduce((acc, { value }) => (acc += value), ''), + position: { + start: jsxNodes[0].position.start, + end: last(jsxNodes).position.end, + }, + }) + jsxNodes.length = 0 + } + } + } else if (offset) { + jsxNodes.push(node) + } else { + acc.push(node) + } + return acc + }, []) + } + traverse(node: Node, parent?: Parent) { if (!node) { return @@ -23,7 +87,7 @@ export class Traverse { const children = node.children as Node[] if (children) { - ;(node.children = combineJsxNodes(children)).forEach(child => + ;(node.children = this.combineJsxNodes(children)).forEach(child => this.traverse(child, node as Parent), ) } diff --git a/test/fixture1.mdx b/test/fixture1.mdx index e0a78a46..c6439e9e 100644 --- a/test/fixture1.mdx +++ b/test/fixture1.mdx @@ -1,27 +1,5 @@ import Component from './component' -export const meta = { - title: 'Blog Post', -} - -# Blog Post - -Lorem ipsum dolor sit amet, consectetur adipiscing **elit**. Ut ac lobortis velit. - - - -```css -@media (min-width: 400px) { - border-color: #000; -} -``` - -{/* This is a comment */} - - - {/** This is a comment */}In JSX!{/** This is a comment */} - - -## Subtitle - -Lorem ipsum dolor sit _amet_, consectetur adipiscing elit. Ut ac lobortis velit. +Inline In > + JSX! > diff --git a/types.d.ts b/types.d.ts index 20cce13b..cfe07138 100644 --- a/types.d.ts +++ b/types.d.ts @@ -10,6 +10,12 @@ declare module 'eslint/lib/rules/no-unused-expressions' { export = noUnUsedExpressions } +declare module 'eslint-plugin-react/lib/rules/no-unescaped-entities' { + import { Rule } from 'eslint' + const reactNoUnEscapedEntities: Rule.RuleModule + export = reactNoUnEscapedEntities +} + declare module 'espree' { import * as estree from 'estree' diff --git a/yarn.lock b/yarn.lock index c109267c..434c5b40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@babel/code-frame@7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" + integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== + dependencies: + "@babel/highlight" "^7.0.0" + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" @@ -1912,6 +1919,17 @@ eslint-config-standard@^13.0.1: resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-13.0.1.tgz#c9c6ffe0cfb8a51535bc5c7ec9f70eafb8c6b2c0" integrity sha512-zLKp4QOgq6JFgRm1dDCVv1Iu0P5uZ4v5Wa4DTOkg2RFMxdCX/9Qf7lz9ezRj2dBRa955cWQF/O/LWEiYWAHbTw== +eslint-formatter-friendly@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/eslint-formatter-friendly/-/eslint-formatter-friendly-7.0.0.tgz#32a4998ababa0a39994aed629b831fda7dabc864" + integrity sha512-WXg2D5kMHcRxIZA3ulxdevi8/BGTXu72pfOO5vXHqcAfClfIWDSlOljROjCSOCcKvilgmHz1jDWbvFCZHjMQ5w== + dependencies: + "@babel/code-frame" "7.0.0" + chalk "2.4.2" + extend "3.0.2" + strip-ansi "5.2.0" + text-table "0.2.0" + eslint-import-resolver-node@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" @@ -2228,7 +2246,7 @@ extend-shallow@^3.0.0, extend-shallow@^3.0.2: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@~3.0.2: +extend@3.0.2, extend@^3.0.0, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== @@ -5722,6 +5740,13 @@ stringify-package@1.0.0: resolved "https://registry.yarnpkg.com/stringify-package/-/stringify-package-1.0.0.tgz#e02828089333d7d45cd8c287c30aa9a13375081b" integrity sha512-JIQqiWmLiEozOC0b0BtxZ/AOUtdUZHCBPgqIZ2kSJJqGwgb9neo44XdTHUC4HZSGqi03hOeB7W/E8rAlKnGe9g== +strip-ansi@5.2.0, strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" @@ -5736,13 +5761,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -5859,7 +5877,7 @@ text-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.0.0.tgz#43eabd1b495482fae4a2bf65e5f56c29f69220f6" integrity sha512-F91ZqLgvi1E0PdvmxMgp+gcf6q8fMH7mhdwWfzXnl1k+GbpQDmi8l7DzLC5JTASKbwpY3TfxajAUzAXcv2NmsQ== -text-table@^0.2.0: +text-table@0.2.0, text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=