You are an AI programming assistant.
- Follow the user's requirements carefully & to the letter.
- First think step-by-step -- describe your plan for what to build in psuedocode, written out in great detail
- Then output the code in a single codeblock
- Minimize any other prose

I am writing a conformance test (similar to ESLint) in TypeScript for detecting when a user does not use `rel="noopener"` when using `window.open`. The issue this causes is breaking the bfcache integrity in the browser. Some more details here:

```
# Avoid window.opener references

In some browsers (including Chromium-based browsers) if a page was opened using window.open() or (in Chromium-based browsers prior to version 88) from a link with target=\_blank—without specifying rel="noopener"—then the opening page will have a reference to the window object of the opened page.

In addition to being a security risk, a page with a non-null window.opener reference cannot safely be put into the bfcache because that could break any pages attempting to access it.

As a result, it's best to avoid creating window.opener references by using rel="noopener" whenever possible. If your site requires opening a window and controlling it through window.postMessage() or directly referencing the window object, neither the opened window nor the opener will be eligible for bfcache.
```

Using the below example rule that looks at preventing a user from using the browser `unload` event, code up the rule for creating an error when a user uses `window.open` without `rel="noopener"`:

---
import { stripIndents } from 'common-tags';
import type { AST } from '@typescript-eslint/typescript-estree';
import {
 AST\_NODE\_TYPES,
 simpleTraverse,
} from '@typescript-eslint/typescript-estree';
import type { ConformanceError } from '../conformance-error';
import type { ProjectData } from '../project-data';
import { convertPositionToRange } from '../utils/convert-position-to-range';

const TEST\_NAME = 'BFCACHE\_INTEGRITY\_NO\_UNLOAD\_LISTENERS';

const ERROR\_MESSAGE = stripIndents`
 The \`unload\` event does not fire reliably and listening for it can prevent browser optimizations like the Back-Forward Cache (bfcache).

 Consider using the \`pagehide\`, \`visibilitychange\`, or \`beforeunload\` events instead.

 See https://web.dev/bfcache for more information.
`;

export function noUnloadListeners(
 projectData: ProjectData,
 filePath: string,
 ast: AST<{
 filePath: string;
 loc: true;
 range: false;
 }>,
): ConformanceError[] {
 const conformanceConfig = projectData.getConformanceConfig();
 if (!conformanceConfig.isTestEnabled(TEST\_NAME)) return [];

 const workspaceName = projectData.getWorkspaceName();
 const errors: ConformanceError[] = [];

 simpleTraverse(ast, {
 // Detect `window.onunload =` assignments.
 AssignmentExpression(node, \_parent) {
 if (node.type !== AST\_NODE\_TYPES.AssignmentExpression) return;

 const { left } = node;
 if (left.type !== AST\_NODE\_TYPES.MemberExpression) return;

 const { object, property } = left;
 if (object.type !== AST\_NODE\_TYPES.Identifier) return;
 if (object.name !== 'window') return;
 if (property.type !== AST\_NODE\_TYPES.Identifier) return;
 if (property.name !== 'onunload') return;

 errors.push({
 testName: TEST\_NAME,
 location: {
 workspace: workspaceName,
 filePath,
 range: convertPositionToRange(node.loc),
 },
 message: ERROR\_MESSAGE,
 });
 },
 // Detect `window.addEventListener('unload')` calls.
 CallExpression(node, \_parent) {
 if (node.type !== AST\_NODE\_TYPES.CallExpression) return;

 const { callee } = node;
 if (callee.type !== AST\_NODE\_TYPES.MemberExpression) return;

 const { object, property } = callee;
 if (object.type !== AST\_NODE\_TYPES.Identifier) return;
 if (object.name !== 'window') return;
 if (property.type !== AST\_NODE\_TYPES.Identifier) return;
 if (property.name !== 'addEventListener') return;

 const [eventTypeNode] = node.arguments;
 if (eventTypeNode?.type !== AST\_NODE\_TYPES.Literal) return;
 if (eventTypeNode.value !== 'unload') return;

 errors.push({
 testName: TEST\_NAME,
 location: {
 workspace: workspaceName,
 filePath,
 range: convertPositionToRange(node.loc),
 },
 message: ERROR\_MESSAGE,
 });
 },
 });

 return errors;
}
---
당신은 AI 프로그래밍 도우미입니다.- 사용자의 요구 사항을 주의 깊고 정확하게 따르세요.- 먼저 단계별로 생각하세요. 무엇을 만들 것인지에 대한 계획을 psuedocode로 매우 상세하게 작성하세요.- 그런 다음 코드를 하나의 코드 블록으로 출력하세요.- 다른 산문은 최소화하세요.사용자가 'window.open'을 사용할 때 `rel="noopener"`를 사용하지 않는 경우를 감지하기 위한 적합성 테스트(ESLint와 유사)를 TypeScript로 작성하고 있습니다. 이로 인해 발생하는 문제는 브라우저의 bfcache 무결성을 깨뜨리는 것입니다. 자세한 내용은 여기를 참조하세요:```# window.opener 참조 피하기일부 브라우저(Chromium 기반 브라우저 포함)에서, target=\_blank가 있는 링크에서 window.open() 또는 (버전 88 이전 Chromium 기반 브라우저의 경우) rel="noopener"를 지정하지 않고 페이지를 열면 열린 페이지의 창 개체에 대한 참조가 생깁니다.보안상 위험할 뿐만 아니라, 널이 아닌 window.opener 참조가 있는 페이지는 해당 페이지에 액세스하려는 모든 페이지를 손상시킬 수 있으므로 bfcache에 안전하게 저장할 수 없습니다.따라서 가능하면 rel="noopener"를 사용하여 window.opener 참조를 만들지 않는 것이 가장 좋습니다. 사이트에서 창을 열고 window.postMessage()를 통해 창을 제어하거나 창 개체를 직접 참조해야 하는 경우 열린 창이나 오프너 모두 bfcache에 사용할 수 없습니다.```사용자가 브라우저 `언로드` 이벤트를 사용하지 못하도록 하는 아래 예제 규칙을 사용하여 사용자가 `rel="noopener"` 없이 `window.open`을 사용할 때 오류를 생성하는 규칙을 코딩합니다:---import { stripIndents } 를 가져옵니다;'@typescript-eslint/typescript-estree'에서 유형 { AST }를 임포트합니다;import { AST\_NODE\_TYPES, simpleTraverse,}를 '@typescript-eslint/typescript-estree'에서 임포트합니다;'../conformance-error'에서 유형 { ConformanceError }를 임포트합니다;'../project-data'에서 { ProjectData } 유형 가져 오기;'../utils/convert-position-to-range'에서 { convertPositionToRange }를 임포트합니다;const TEST\_NAME = 'BFCACHE\_INTEGRITY\_NO\_UNLOAD\_LISTENERS';const ERROR\_MESSAGE = stripIndents` 'unload\` 이벤트는 안정적으로 발생하지 않으며, 이를 수신하면 역방향 캐시(bfcache)와 같은 브라우저 최적화를 방해할 수 있습니다. 대신 \`pagehide\`, \`visibilitychange\` 또는 \`beforeunload\` 이벤트를 사용하는 것이 좋습니다. 자세한 내용은 https://web.dev/bfcache 을 참조하세요.`;내보내기 함수 noUnloadListeners( projectData: 프로젝트데이터, filePath: 문자열, ast: AST<{ 파일경로: 문자열; loc: true; 범위: 거짓; }>,): ConformanceError[] { const conformanceConfig = projectData.getConformanceConfig(); if (!conformanceConfig.isTestEnabled(TEST\_NAME)) return []; const workspaceName = projectData.getWorkspaceName(); 에러를 생성합니다: ConformanceError[] = []; simpleTraverse(ast, { // `window.onunload =` 할당을 감지합니다. AssignmentExpression(node, \_parent) { if (node.type !== AST\_NODE\_TYPES.AssignmentExpression) return; const { left } = 노드; if (left.type !== AST\_NODE\_TYPES.MemberExpression) return; const { object, property } = left; if (object.type !== AST\_NODE\_TYPES.Identifier) return; if (object.name !== 'window') return; if (property.type !== AST\_NODE\_TYPES.Identifier) return; if (property.name !== 'onunload') return; errors.push({ testName: TEST\_NAME, location: { 작업 공간: 작업 공간 이름, filePath, 범위: convertPositionToRange(node.loc), }, message: error\_message, }); }, // `window.addEventListener('unload')` 호출을 감지합니다. CallExpression(node, \_parent) { if (node.type !== AST\_NODE\_TYPES.CallExpression) return; const { 칼리 } = node; if (callee.type !== AST\_NODE\_TYPES.MemberExpression) return; const { 객체, 속성 } = callee; if (object.type !== AST\_NODE\_TYPES.Identifier) return; if (object.name !== 'window') return; if (property.type !== AST\_NODE\_TYPES.Identifier) return; if (property.name !== 'addEventListener') return; const [eventTypeNode] = node.arguments; if (eventTypeNode?.type !== AST\_NODE\_TYPES.Literal) return; if (eventTypeNode.value !== 'unload') return; errors.push({ testName: TEST\_NAME, location: { 작업 공간: 작업 공간 이름, filePath, 범위: convertPositionToRange(node.loc), }, message: error\_message, }); }, }); 에러를 반환합니다;}---