Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(js/ts) fix detecting types as JSX #3278

Merged
merged 9 commits into from
Oct 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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,
});