Skip to content

linter: .astro files have false positive Expected on parse of file with JS-in-Frontmatter #22090

Description

@shellscape

What version of Oxlint are you using?

1.62.0

What command did you run?

oxlint --format stylish --tsconfig tsconfig.json apps infra packages scripts services

What does your .oxlintrc.json (or oxlint.config.ts) config file look like?

{
  "$schema": "./node_modules/oxlint/configuration_schema.json",

  "env": {
    "browser": true,
    "es2022": true,
    "node": true
  },

  "ignorePatterns": [
    "**/*.coffee",
    "**/*.css",
    "**/*.hbs",
    "**/*.json",
    "**/*.less",
    "**/*.scss",
    "**/*.svg",
    "dist/**",
    "node_modules/**"
  ],

  "overrides": [
    {
      "files": ["apps/**/*.*"],
      "globals": {
        "Astro": "readonly"
      }
    },
    {
      "files": ["**/*.{astro,mdx}"],
      "globals": {
        "Astro": "readonly"
      }
    },
    {
      "files": ["**/test/**/*.{js,jsx,ts,tsx}", "**/tests/**/*.{js,jsx,ts,tsx}"],
      "env": {
        "jest": true
      },
      "globals": {
        "describe": "readonly",
        "it": "readonly",
        "test": "readonly",
        "expect": "readonly",
        "vi": "readonly"
      }
    }
  ],

  "rules": {
    // correctness/*
    "no-array-constructor": "error",
    "no-class-assign": "error",
    "no-cond-assign": "error",
    "no-const-assign": "error",
    "no-constant-condition": "warn",
    "no-control-regex": "error",
    "no-debugger": "error",
    "no-dupe-class-members": "error",
    "no-dupe-keys": "error",
    "no-duplicate-case": "error",
    "no-empty-character-class": "error",
    "no-empty-pattern": "error",
    "no-ex-assign": "error",
    "no-func-assign": "error",
    "no-invalid-regexp": "error",
    "no-irregular-whitespace": "error",
    "no-obj-calls": "error",
    "no-prototype-builtins": "error",
    "no-regex-spaces": "error",
    "no-self-assign": "error",
    "no-sparse-arrays": "error",
    "no-template-curly-in-string": "error",
    "no-this-before-super": "error",
    "no-undef": "error",
    "no-unreachable": "error",
    "no-unsafe-finally": "error",
    "no-unsafe-negation": "error",
    "require-yield": "error",
    "use-isnan": "error",
    "valid-typeof": "error",

    // performance/*
    "no-new-buffer": "error",

    // style/*
    "capitalized-comments": "off",
    "eqeqeq": ["error", "always", { "null": "ignore" }],
    "new-cap": ["error", { "capIsNew": false, "newIsCap": true }],
    "no-bitwise": "error",
    "no-continue": "error",
    "no-else-return": "error",
    "no-lonely-if": "error",
    "no-multi-assign": "error",
    "no-object-constructor": "error",
    "no-param-reassign": ["warn", { "props": false }],
    "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
    "no-unneeded-ternary": ["error", { "defaultAssignment": false }],
    "no-var": "error",
    "object-shorthand": ["error", "always", { "avoidQuotes": true, "ignoreConstructors": false }],
    "operator-assignment": ["error", "always"],
    "prefer-const": ["error", { "destructuring": "any", "ignoreReadBeforeAssign": true }],
    "prefer-destructuring": ["warn", { "array": false, "object": true }],
    "prefer-numeric-literals": "error",
    "prefer-rest-params": "error",
    "prefer-spread": "error",
    "prefer-template": "error",
    "symbol-description": "error",
    "vars-on-top": "error",
    "yoda": "error",

    // suspicious/*
    "no-await-in-loop": "error",
    "no-compare-neg-zero": "error",
    "no-empty": ["error", { "allowEmptyCatch": true }],
    "no-empty-function": ["error", { "allow": ["arrowFunctions", "functions", "methods"] }],
    "no-eval": "error",
    "no-extend-native": "error",
    "no-extra-bind": "error",
    "no-extra-boolean-cast": "error",
    "no-fallthrough": "error",
    "no-global-assign": "error",
    "no-implied-eval": "error",
    "no-inner-declarations": "error",
    "no-iterator": "error",
    "no-labels": "error",
    "no-lone-blocks": "error",
    "no-loop-func": "error",
    "no-multi-str": "error",
    "no-new": "error",
    "no-new-func": "error",
    "no-new-wrappers": "error",
    "no-return-assign": "error",
    "typescript/return-await": ["error", "in-try-catch"],
    "no-script-url": "error",
    "no-self-compare": "error",
    "no-sequences": "error",
    "no-throw-literal": "error",
    "no-unmodified-loop-condition": "off",
    "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": false }],
    "no-unused-labels": "error",
    "no-useless-concat": "error",
    "no-useless-constructor": "error",
    "no-useless-escape": "error",
    "no-useless-return": "error",
    "no-void": "off",
    "no-with": "error",

    // unicorn/*
    "unicorn/no-new-array": "off",
    "unicorn/no-useless-fallback-in-spread": "off"
  }
}

What happened?

oxlint is presently throwing a parsing error for .astro files which have JS/TS within frontmatter. The error is reported as

`Expected `>` but found `Identifier` 

The error can be observed on this workflow run: https://github.com/shellscape/jsx-email/actions/runs/25264382270/job/74076619326#step:6:12
And can be reproduced by this file content:

---
import EndOfContent from "@components/docs/EndOfContent.astro";
import MDXStyles from "@components/docs/MDXStyles.astro";
import BaseHead from "@/ui/BaseHead.astro";
import Header from "@components/docs/Header.astro";
import DocsSidebar from "@components/docs/DocsSidebar.astro";
import { getContentTree } from "@/utils";
import type { IHeading } from "@/types";
import DialectTabs from "@components/docs/DialectTabs.astro";
import { dialects } from "@/data/dialects";

interface Props {
  customTitle?: string;
  customSlug?: string;
  description?: string;
  headings: IHeading[];
}

const { customTitle, headings, description, customSlug } = Astro.props;
const { slug } = Astro.params;
const { tree, filteredHeadings, title } = await getContentTree({
  headings,
  slug,
});

const pageHasDialects = Object.keys(dialects).includes(
  slug?.split("/").at(-1)!,
);
---

<!doctype html>
<html lang="en">
  <BaseHead
    title={`JSX email - ${customTitle || title}`}
    description={description}
  />
  <body class="banner-shown docs-body">
    <MDXStyles />
    <div class="docs-background" aria-hidden="true" />
    <Header />
    <div class="fixed-container">
      <div class="fixed-elements">
        <DocsSidebar
          tree={tree}
          customSlug={customSlug}
          headings={filteredHeadings}
        />
        <div class="dialects-container">
          {pageHasDialects && <DialectTabs />}
        </div>
      </div>
    </div>
    <main class="documentation-container">
      <div class="documentation-content">
        {pageHasDialects && <div style="height: 52px" />}
        <slot />
        <EndOfContent />
        <!-- <Footer /> -->
      </div>
    </main>
    <style is:global>
      .docs-body {
        position: relative;
        isolation: isolate;
      }

      .docs-background {
        position: absolute;
        top: 8rem;
        right: 0;
        z-index: -1;
        width: min(1200px, 90vw);
        aspect-ratio: 1200 / 669;
        transform: translateX(70%);
        background-image: url("/landing-bg-muted.svg");
        background-repeat: no-repeat;
        background-size: contain;
        background-position: right top;
        opacity: 0.9;
        pointer-events: none;
        user-select: none;
      }

      .fixed-container {
        position: fixed;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 99;
        pointer-events: none;
      }

      .fixed-elements {
        max-width: 1440px;
        width: 100%;
        justify-content: space-between;
        display: flex;
        margin: 0 auto;
        padding-left: 24px;
        padding-right: 22px;
      }

      .right-aside-container {
        pointer-events: all;
      }

      .navigation_nav_container_blur {
        -webkit-backdrop-filter: blur(12px);
        backdrop-filter: blur(12px);
        pointer-events: none;
        position: absolute;
        z-index: -1;
        height: 100%;
        width: 100%;
        background-color: hsla(0, 0%, 100%, 0.85);
        box-shadow:
          0 0 0 0 transparent,
          0 0 0 0 transparent,
          0 2px 4px 0 rgba(0, 0, 0, 0.02),
          0 1px 0 0 rgba(0, 0, 0, 0.06);
      }
      .navigation_nav_content_wrap {
        margin-left: auto;
        margin-right: auto;
        display: flex;
        height: 64px;
        max-width: 1440px;
        align-items: center;
        gap: 8px;
        padding-left: max(env(safe-area-inset-left), 24px);
        padding-right: max(env(safe-area-inset-right), 24px);
      }
      .documentation-container {
        max-width: 1440px;
        width: 100%;
        display: flex;
        margin: 0 auto;
        padding-left: calc(240px + 24px);
        padding-right: calc(240px + 24px);
      }

      .documentation-content {
        padding-left: 48px;
        padding-right: 48px;
        padding-top: 1rem;
        overflow-x: hidden;
        width: 100%;
      }

      .dialects-container {
        padding-left: 48px;
        padding-right: 48px;
        padding-top: 1rem;
        overflow-x: hidden;
        width: 100%;
      }

      @media screen and (max-width: 1279px) {
        .documentation-content {
          padding-left: 16px;
          padding-right: 16px;
        }

        .dialects-container {
          padding-left: 16px;
          padding-right: 16px;
        }
      }

      @media screen and (max-width: 1024px) {
        .right-aside-container {
          display: none;
        }

        .documentation-container {
          padding-right: 24px;
        }

        .documentation-content {
          padding-right: 0;
        }

        .dialects-container {
          padding-right: 0;
        }
      }

      @media screen and (max-width: 768px) {
        .docs-background {
          top: 6rem;
          width: min(960px, 120vw);
        }

        .documentation-content {
          padding-left: 16px;
          padding-right: 16px;
          padding-top: 1rem;
          width: 100%;
        }

        .dialects-container {
          padding-left: 16px;
          padding-right: 16px;
        }

        .documentation-container {
          padding: 0;
        }

        .fixed-elements {
          padding: 0;
        }
      }
    </style>
    <style>
      @media screen and (max-width: 1024px) {
        .right-aside-container {
          display: none;
        }
      }
    </style>
    <script>
      const hash = window.location.hash;
      if (hash !== "") {
        const targetElement = document.querySelector(hash);
        if (targetElement) {
          setTimeout(() => targetElement.scrollIntoView(), 10);
        }
      }
    </script>
    <script is:inline>
      const changeNpmTab = () => {
        const packageManagers = ["npm", "yarn", "pnpm", "bun"];

        let packageManagerIndex = packageManagers.indexOf(packageManager);

        const updateUI = (index) => {
          npmContent.forEach((element) => {
            Array.from(element.children).forEach((child, i) => {
              child.classList.toggle("hidden", i !== index);
            });
          });
          npmTabs.forEach((element) => {
            Array.from(element.children).forEach((child, i) => {
              child.classList.toggle("npm__tab--active", i === index);
            });
          });

          npxSelectResizer.querySelector(".line span").innerText =
            packageManagers[index];

          npxSelects.forEach((element) => {
            element.value = index;
            element.style.width = `${npxSelectResizer.offsetWidth}px`;
          });
        };

        const npmContent = document.querySelectorAll(".npm__content");
        const npmTabs = document.querySelectorAll(".npm__tabs");
        const npxSelects = document.querySelectorAll(".npx-select");
        const npxSelectResizer = document.querySelector(".npx-select-resizer");

        npmContent.forEach((content) => {
          Array.from(content.children).forEach((child, index) => {
            child.classList.toggle("hidden", index !== packageManagerIndex);
          });
        });

        npmTabs.forEach((tab) => {
          Array.from(tab.children).forEach((child, index) => {
            child.classList.toggle(
              "npm__tab--active",
              index === packageManagerIndex,
            );
            child.addEventListener("click", () => {
              packageManager = packageManagers[index];
              localStorage.setItem("package-manager", packageManager);
              updateUI(index);
            });
          });
        });

        npxSelects.forEach((element) => {
          npxSelectResizer.querySelector(".line span").innerText =
            packageManagers[packageManagerIndex];
          element.value = packageManagerIndex;
          setTimeout(
            () => (element.style.width = `${npxSelectResizer.offsetWidth}px`),
            1,
          );
          element.addEventListener("change", (e) => {
            packageManager = packageManagers[Number(e.target.value)];
            localStorage.setItem("package-manager", packageManager);
            updateUI(Number(e.target.value));
          });
        });
      };

      const updateCollapsedSections = () => {
        document
          .querySelectorAll(".nav-items-collapsable")
          .forEach((section) => {
            if (
              Number(section.getAttribute("data-active-inner-item")) &&
              !expandedSections.find((v) => v === section.id)
            ) {
              expandedSections = [...expandedSections, section.id];
            }
            if (expandedSections.find((v) => v === section.id)) {
              section.classList.add("expanded");
            } else {
              section.classList.remove("expanded");
            }
          });

        localStorage.setItem(
          "expandedSections",
          JSON.stringify(expandedSections),
        );
      };

      const scrollSidebar = () => {
        const expandedSections = localStorage.getItem("expandedSections")
          ? JSON.parse(localStorage.getItem("expandedSections"))
          : [];

        document
          .querySelectorAll(".nav-items-collapsable")
          .forEach((section) => {
            if (expandedSections.find((v) => v === section.id)) {
              section.classList.add("expanded");
            } else {
              section.classList.remove("expanded");
            }
          });

        const leftSidebar = document.querySelector(".aside-scroll");
        const navItems = document.querySelectorAll("[data-nav-index]");
        const headingsContainer = document.querySelector("[data-headings]");
        const headingsContainerHeight = headingsContainer?.clientHeight || 0;
        let itemIndex = 0;

        const activeNavItem = Array.from(navItems).find((item) => {
          return item.classList.contains("nav-item--active");
        });

        if (activeNavItem) {
          itemIndex = +(activeNavItem.getAttribute("data-nav-index") || 0);
        }
        if (navItems && leftSidebar) {
          navItems.forEach((item) => {
            item.addEventListener("click", () => {
              const nextIndex = +item.getAttribute("data-nav-index");
              const scrollTop =
                leftSidebar.scrollTop -
                (nextIndex > itemIndex ? headingsContainerHeight : 0);
              localStorage.setItem("sidebar-scroll", scrollTop.toString());
            });
          });

          const findPreviousNavSeparator = (element) => {
            if (!element) return null;

            let previousElement = element.previousElementSibling;

            while (previousElement) {
              if (previousElement.classList.contains("nav-separator")) {
                return previousElement;
              }
              previousElement = previousElement.previousElementSibling;
            }

            return null;
          };

          const leftSidebarScroll = localStorage.getItem("sidebar-scroll");
          if (leftSidebarScroll !== null) {
            leftSidebar.scrollTop = parseInt(leftSidebarScroll, 10);
          } else if (activeNavItem) {
            if (
              findPreviousNavSeparator(activeNavItem)?.classList.contains(
                "nav-separator-collapsable",
              )
            ) {
              leftSidebar.scrollTop =
                findPreviousNavSeparator(activeNavItem).parentElement.offsetTop;
            } else {
              leftSidebar.scrollTop =
                findPreviousNavSeparator(activeNavItem).offsetTop;
            }
          }
        }
      };

      scrollSidebar();
      updateCollapsedSections();
      changeNpmTab();
    </script>
    <script>
      class Rem1Element extends HTMLElement {
        constructor() {
          super();
        }
      }

      class Rem025Element extends HTMLElement {
        constructor() {
          super();
        }
      }

      class Rem05Element extends HTMLElement {
        constructor() {
          super();
        }
      }

      customElements.define("rem-1", Rem1Element);
      customElements.define("rem-025", Rem025Element);
      customElements.define("rem-05", Rem05Element);
    </script>
    <script is:inline data-astro-rerun>
      document.querySelectorAll("h2").forEach((el) => {
        if (el.innerText === "---") {
          el.classList.add("hidden-separator");
        }
      });
    </script>
    <script>
      document.addEventListener("astro:page-load", () => {
        const copyBtn = document.querySelectorAll(
          "[data-copy-btn]",
        ) as NodeListOf<HTMLElement>;
        copyBtn.forEach((n) => {
          n.addEventListener("click", () => {
            let timeouts: NodeJS.Timeout[] = [];

            if (timeouts.length) {
              timeouts.forEach((id) => {
                clearTimeout(id);
              });
            }

            navigator.clipboard.writeText(n.dataset.code!);

            n.classList.add("copied");
            n.classList.add("hide-svg");
            timeouts.push(
              setTimeout(() => {
                n.classList.remove("copied");
              }, 1500),
            );
            timeouts.push(
              setTimeout(() => {
                n.classList.remove("hide-svg");
              }, 2000),
            );
          });
        });
      });
    </script>
    <script>
      document.addEventListener("astro:after-swap", () => {
        const npmContent: NodeList = document.querySelectorAll(".npm__content");
        const npmTabs: NodeList = document.querySelectorAll(".npm__tabs");
        const npxSelects =
          document.querySelectorAll<HTMLSelectElement>(".npx-select");
        const npxSelectResizer = document.querySelector<HTMLDivElement>(
          ".npx-select-resizer",
        );
        const packageManagers: string[] = ["npm", "yarn", "pnpm", "bun"];
        let packageManager: string =
          localStorage.getItem("package-manager") || "npm";
        let packageManagerIndex: number =
          packageManagers.indexOf(packageManager);

        const updateUI = (index: number): void => {
          npmContent.forEach((element) => {
            Array.from((element as HTMLElement).children).forEach(
              (child, i) => {
                child.classList.toggle("hidden", i !== index);
              },
            );
          });
          npmTabs.forEach((element) => {
            Array.from((element as HTMLElement).children).forEach(
              (child, i) => {
                child.classList.toggle("npm__tab--active", i === index);
              },
            );
          });
          const lineSpan =
            npxSelectResizer?.querySelector<HTMLSpanElement>(".line span");
          if (lineSpan) {
            lineSpan.innerText = packageManagers[index];
          }

          npxSelects.forEach((element) => {
            element.value = index.toString();
            if (npxSelectResizer) {
              element.style.width = `${npxSelectResizer.offsetWidth}px`;
            }
          });
        };

        npmContent.forEach((content) => {
          Array.from((content as HTMLElement).children).forEach(
            (child, index) => {
              child.classList.toggle("hidden", index !== packageManagerIndex);
            },
          );
        });

        npmTabs.forEach((tab) => {
          Array.from((tab as HTMLElement).children).forEach((child, index) => {
            child.classList.toggle(
              "npm__tab--active",
              index === packageManagerIndex,
            );
            child.addEventListener("click", () => {
              packageManager = packageManagers[index];
              localStorage.setItem("package-manager", packageManager);
              updateUI(index);
            });
          });
        });

        npxSelects.forEach((element) => {
          const lineSpan =
            npxSelectResizer?.querySelector<HTMLSpanElement>(".line span");
              if(lineSpan) {
                lineSpan.innerText = packageManagers[packageManagerIndex];
              }

          element.value = packageManagerIndex.toString();
          if (npxSelectResizer) {
            element.style.width = `${npxSelectResizer.offsetWidth}px`;
          }
          element.addEventListener("change", (e) => {
            const selectElement = e.target as HTMLSelectElement;
            packageManager = packageManagers[Number(selectElement.value)];
            localStorage.setItem("package-manager", packageManager);
            updateUI(Number(selectElement.value));
          });
        });
      });
    </script>
    <style is:global>
      rem-1 {
        display: block;
        height: 1rem;
      }

      rem-025 {
        display: block;
        height: 0.25rem;
      }

      rem-05 {
        display: block;
        height: 0.5rem;
      }

      .hidden-separator {
        width: 0;
        height: 0;
        pointer-events: none;
        overflow: hidden;
        margin: 0 !important;
        padding: 0 !important;
      }
    </style>
  </body>
</html>

Commenting out the frontmatter JS naturally resolves the issue being reported but causes a litany of other linting errors.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Fields

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions