Skip to content

Commit

Permalink
fix(js/ts) fix detecting types as JSX (#3278)
Browse files Browse the repository at this point in the history
  • Loading branch information
joshgoebel committed Oct 17, 2021
1 parent 0f70132 commit fbf9534
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 26 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Expand Up @@ -18,6 +18,8 @@ Parser:

Grammars:

- fix(ts) some complex types would classify as JSX (#3278) [Josh Goebel][]
- fix(js/ts) less false positives for `class X extends Y` (#3278) [Josh Goebel][]
- enh(css): add properties from several W3C (Candidate) Recommendations (#3308)
- fix(js/ts) `Float32Array` highlighted incorrectly (#3353) [Josh Goebel][]
- fix(css) single-colon psuedo-elements no longer break highlighting (#3240) [Josh Goebel][]
Expand Down
59 changes: 46 additions & 13 deletions src/languages/javascript.js
Expand Up @@ -28,6 +28,8 @@ export default function(hljs) {
begin: '<>',
end: '</>'
};
// to avoid some special cases inside isTrulyOpeningTag
const XML_SELF_CLOSING = /<[A-Za-z0-9\\._:-]+\s*\/>/;
const XML_TAG = {
begin: /<[A-Za-z0-9\\._:-]+/,
end: /\/[A-Za-z0-9\\._:-]+>|\/>/,
Expand All @@ -38,22 +40,43 @@ export default function(hljs) {
isTrulyOpeningTag: (match, response) => {
const afterMatchIndex = match[0].length + match.index;
const nextChar = match.input[afterMatchIndex];
// nested type?
// HTML should not include another raw `<` inside a tag
// But a type might: `<Array<Array<number>>`, etc.
if (nextChar === "<") {
if (
// HTML should not include another raw `<` inside a tag
// nested type?
// `<Array<Array<number>>`, etc.
nextChar === "<" ||
// the , gives away that this is not HTML
// `<T, A extends keyof T, V>`
nextChar === ",") {
response.ignoreMatch();
return;
}
// <something>
// This is now either a tag or a type.

// `<something>`
// Quite possibly a tag, lets look for a matching closing tag...
if (nextChar === ">") {
// if we cannot find a matching closing tag, then we
// will ignore it
if (!hasClosingTag(match, { after: afterMatchIndex })) {
response.ignoreMatch();
}
}

// `<blah />` (self-closing)
// handled by simpleSelfClosing rule

// `<From extends string>`
// technically this could be HTML, but it smells like a type
let m;
const afterMatch = match.input.substr(afterMatchIndex);
// NOTE: This is ugh, but added specifically for https://github.com/highlightjs/highlight.js/issues/3276
if ((m = afterMatch.match(/^\s+extends\s+/))) {
if (m.index === 0) {
response.ignoreMatch();
// eslint-disable-next-line no-useless-return
return;
}
}
}
};
const KEYWORDS = {
Expand Down Expand Up @@ -227,28 +250,37 @@ export default function(hljs) {
// ES6 classes
const CLASS_OR_EXTENDS = {
variants: [
// class Car extends vehicle
{
match: [
/class/,
/\s+/,
IDENT_RE
IDENT_RE,
/\s+/,
/extends/,
/\s+/,
regex.concat(IDENT_RE, "(", regex.concat(/\./, IDENT_RE), ")*")
],
scope: {
1: "keyword",
3: "title.class"
3: "title.class",
5: "keyword",
7: "title.class.inherited"
}
},
// class Car
{
match: [
/extends/,
/class/,
/\s+/,
regex.concat(IDENT_RE, "(", regex.concat(/\./, IDENT_RE), ")*")
IDENT_RE
],
scope: {
1: "keyword",
3: "title.class.inherited"
3: "title.class"
}
}
},

]
};

Expand Down Expand Up @@ -390,7 +422,7 @@ export default function(hljs) {
aliases: ['js', 'jsx', 'mjs', 'cjs'],
keywords: KEYWORDS,
// this will be extended by TypeScript
exports: { PARAMS_CONTAINS },
exports: { PARAMS_CONTAINS, CLASS_REFERENCE },
illegal: /#(?![$_A-z])/,
contains: [
hljs.SHEBANG({
Expand Down Expand Up @@ -464,6 +496,7 @@ export default function(hljs) {
{ // JSX
variants: [
{ begin: FRAGMENT.begin, end: FRAGMENT.end },
{ match: XML_SELF_CLOSING },
{
begin: XML_TAG.begin,
// we carefully check the opening tag to see if it truly
Expand Down
40 changes: 27 additions & 13 deletions src/languages/typescript.js
Expand Up @@ -12,19 +12,9 @@ import javascript from "./javascript.js";

/** @type LanguageFn */
export default function(hljs) {
const tsLanguage = javascript(hljs);

const IDENT_RE = ECMAScript.IDENT_RE;
const NAMESPACE = {
beginKeywords: 'namespace', end: /\{/, excludeEnd: true
};
const INTERFACE = {
beginKeywords: 'interface', end: /\{/, excludeEnd: true,
keywords: 'interface extends'
};
const USE_STRICT = {
className: 'meta',
relevance: 10,
begin: /^\s*['"]use strict['"]/
};
const TYPES = [
"any",
"void",
Expand All @@ -35,6 +25,31 @@ export default function(hljs) {
"never",
"enum"
];
const NAMESPACE = {
beginKeywords: 'namespace',
end: /\{/,
excludeEnd: true,
contains: [
tsLanguage.exports.CLASS_REFERENCE
]
};
const INTERFACE = {
beginKeywords: 'interface',
end: /\{/,
excludeEnd: true,
keywords: {
keyword: 'interface extends',
built_in: TYPES
},
contains: [
tsLanguage.exports.CLASS_REFERENCE
]
};
const USE_STRICT = {
className: 'meta',
relevance: 10,
begin: /^\s*['"]use strict['"]/
};
const TS_SPECIFIC_KEYWORDS = [
"type",
"namespace",
Expand Down Expand Up @@ -67,7 +82,6 @@ export default function(hljs) {
mode.contains.splice(indx, 1, replacement);
};

const tsLanguage = javascript(hljs);

// this should update anywhere keywords is used since
// it will be the same actual JS object
Expand Down
18 changes: 18 additions & 0 deletions test/markup/javascript/jsx.expect.txt
Expand Up @@ -25,3 +25,21 @@
}

<span class="hljs-keyword">var</span> x = <span class="hljs-number">5</span>;

<span class="hljs-comment">// this is NOT JSX and should not trigger the rule</span>
interface <span class="hljs-title class_">Prefixer</span>&lt;<span class="hljs-title class_">Something</span> <span class="hljs-keyword">extends</span> string&gt; {
(): <span class="hljs-string">`other__<span class="hljs-subst">${Something}</span>`</span>;

<span class="hljs-attr">parse</span>: &lt;<span class="hljs-title class_">From</span> <span class="hljs-keyword">extends</span> string&gt;<span class="hljs-function">(<span class="hljs-params">
value: From
</span>) =&gt;</span> number;
}

<span class="hljs-keyword">const</span> cloneWith = &lt;T, A <span class="hljs-keyword">extends</span> keyof T, V&gt;(
<span class="hljs-attr">i</span>: T,
<span class="hljs-attr">a</span>: A,
<span class="hljs-attr">value</span>: V
): <span class="hljs-title class_">Omit</span>&lt;T, A&gt; &amp; {[K <span class="hljs-keyword">in</span> A]: V} =&gt; ({
...i,
[a]: value,
});
18 changes: 18 additions & 0 deletions test/markup/javascript/jsx.txt
Expand Up @@ -25,3 +25,21 @@ class App extends Component {
}

var x = 5;

// this is NOT JSX and should not trigger the rule
interface Prefixer<Something extends string> {
(): `other__${Something}`;

parse: <From extends string>(
value: From
) => number;
}

const cloneWith = <T, A extends keyof T, V>(
i: T,
a: A,
value: V
): Omit<T, A> & {[K in A]: V} => ({
...i,
[a]: value,
});

0 comments on commit fbf9534

Please sign in to comment.