"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); const ts = require("typescript"); const fs_1 = require("fs"); const path_1 = require("path"); const minimatch_1 = require("minimatch"); // // ############################################################################################# // // A custom typescript checker for the specific task of detecting the use of certain types in a // layer that does not allow such use. For example: // - using DOM globals in common/node/electron-main layer (e.g. HTMLElement) // - using node.js globals in common/browser layer (e.g. process) // // Make changes to below RULES to lift certain files from these checks only if absolutely needed // // ############################################################################################# // // Types we assume are present in all implementations of JS VMs (node.js, browsers) // Feel free to add more core types as you see needed if present in node.js and browsers const CORE_TYPES = [ 'require', 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'console', 'Console', 'Error', 'ErrorConstructor', 'String', 'TextDecoder', 'TextEncoder', 'self', 'queueMicrotask', 'Array', 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array', 'Float32Array', 'Float64Array', 'Uint8ClampedArray', 'BigUint64Array', 'BigInt64Array', 'btoa', 'atob', 'AbortSignal', 'MessageChannel', 'MessagePort', 'URL', 'URLSearchParams' ]; // Types that are defined in a common layer but are known to be only // available in native environments should not be allowed in browser const NATIVE_TYPES = [ 'NativeParsedArgs', 'INativeEnvironmentService', 'AbstractNativeEnvironmentService', 'INativeWindowConfiguration', 'ICommonNativeHostService' ]; const RULES = [ // Tests: skip { target: '**/vs/**/test/**', skip: true // -> skip all test files }, // Common: vs/base/common/platform.ts { target: '**/vs/base/common/platform.ts', allowedTypes: [ ...CORE_TYPES, // Safe access to postMessage() and friends 'MessageEvent', ], disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ 'lib.dom.d.ts', '@types/node' // no node.js ] }, // Common: vs/platform/environment/common/* { target: '**/vs/platform/environment/common/*.ts', allowedTypes: CORE_TYPES, disallowedTypes: [ /* Ignore native types that are defined from here */], disallowedDefinitions: [ 'lib.dom.d.ts', '@types/node' // no node.js ] }, // Common: vs/platform/window/common/window.ts { target: '**/vs/platform/window/common/window.ts', allowedTypes: CORE_TYPES, disallowedTypes: [ /* Ignore native types that are defined from here */], disallowedDefinitions: [ 'lib.dom.d.ts', '@types/node' // no node.js ] }, // Common: vs/platform/native/common/native.ts { target: '**/vs/platform/native/common/native.ts', allowedTypes: CORE_TYPES, disallowedTypes: [ /* Ignore native types that are defined from here */], disallowedDefinitions: [ 'lib.dom.d.ts', '@types/node' // no node.js ] }, // Common: vs/workbench/api/common/extHostExtensionService.ts { target: '**/vs/workbench/api/common/extHostExtensionService.ts', allowedTypes: [ ...CORE_TYPES, // Safe access to global 'global' ], disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ 'lib.dom.d.ts', '@types/node' // no node.js ] }, // Common { target: '**/vs/**/common/**', allowedTypes: CORE_TYPES, disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ 'lib.dom.d.ts', '@types/node' // no node.js ] }, // Browser { target: '**/vs/**/browser/**', allowedTypes: CORE_TYPES, disallowedTypes: NATIVE_TYPES, allowedDefinitions: [ '@types/node/stream/consumers.d.ts' // node.js started to duplicate types from lib.dom.d.ts so we have to account for that ], disallowedDefinitions: [ '@types/node' // no node.js ] }, // Browser (editor contrib) { target: '**/src/vs/editor/contrib/**', allowedTypes: CORE_TYPES, disallowedTypes: NATIVE_TYPES, disallowedDefinitions: [ '@types/node' // no node.js ] }, // node.js { target: '**/vs/**/node/**', allowedTypes: CORE_TYPES, disallowedDefinitions: [ 'lib.dom.d.ts' // no DOM ] }, // Electron (sandbox) { target: '**/vs/**/electron-sandbox/**', allowedTypes: CORE_TYPES, disallowedDefinitions: [ '@types/node' // no node.js ] }, // Electron (renderer): skip { target: '**/vs/**/electron-browser/**', skip: true // -> supports all types }, // Electron (main) { target: '**/vs/**/electron-main/**', allowedTypes: [ ...CORE_TYPES, // --> types from electron.d.ts that duplicate from lib.dom.d.ts 'Event', 'Request' ], disallowedTypes: [ 'ipcMain' // not allowed, use validatedIpcMain instead ], disallowedDefinitions: [ 'lib.dom.d.ts' // no DOM ] } ]; const TS_CONFIG_PATH = (0, path_1.join)(__dirname, '../../', 'src', 'tsconfig.json'); let hasErrors = false; function checkFile(program, sourceFile, rule) { checkNode(sourceFile); function checkNode(node) { if (node.kind !== ts.SyntaxKind.Identifier) { return ts.forEachChild(node, checkNode); // recurse down } const checker = program.getTypeChecker(); const symbol = checker.getSymbolAtLocation(node); if (!symbol) { return; } let _parentSymbol = symbol; while (_parentSymbol.parent) { _parentSymbol = _parentSymbol.parent; } const parentSymbol = _parentSymbol; const text = parentSymbol.getName(); if (rule.allowedTypes?.some(allowed => allowed === text)) { return; // override } if (rule.disallowedTypes?.some(disallowed => disallowed === text)) { const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); console.log(`[build/lib/layersChecker.ts]: Reference to type '${text}' violates layer '${rule.target}' (${sourceFile.fileName} (${line + 1},${character + 1})`); hasErrors = true; return; } const declarations = symbol.declarations; if (Array.isArray(declarations)) { DeclarationLoop: for (const declaration of declarations) { if (declaration) { const parent = declaration.parent; if (parent) { const parentSourceFile = parent.getSourceFile(); if (parentSourceFile) { const definitionFileName = parentSourceFile.fileName; if (rule.allowedDefinitions) { for (const allowedDefinition of rule.allowedDefinitions) { if (definitionFileName.indexOf(allowedDefinition) >= 0) { continue DeclarationLoop; } } } if (rule.disallowedDefinitions) { for (const disallowedDefinition of rule.disallowedDefinitions) { if (definitionFileName.indexOf(disallowedDefinition) >= 0) { const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart()); console.log(`[build/lib/layersChecker.ts]: Reference to symbol '${text}' from '${disallowedDefinition}' violates layer '${rule.target}' (${sourceFile.fileName} (${line + 1},${character + 1})`); hasErrors = true; return; } } } } } } } } } } function createProgram(tsconfigPath) { const tsConfig = ts.readConfigFile(tsconfigPath, ts.sys.readFile); const configHostParser = { fileExists: fs_1.existsSync, readDirectory: ts.sys.readDirectory, readFile: file => (0, fs_1.readFileSync)(file, 'utf8'), useCaseSensitiveFileNames: process.platform === 'linux' }; const tsConfigParsed = ts.parseJsonConfigFileContent(tsConfig.config, configHostParser, (0, path_1.resolve)((0, path_1.dirname)(tsconfigPath)), { noEmit: true }); const compilerHost = ts.createCompilerHost(tsConfigParsed.options, true); return ts.createProgram(tsConfigParsed.fileNames, tsConfigParsed.options, compilerHost); } // // Create program and start checking // const program = createProgram(TS_CONFIG_PATH); for (const sourceFile of program.getSourceFiles()) { for (const rule of RULES) { if ((0, minimatch_1.match)([sourceFile.fileName], rule.target).length > 0) { if (!rule.skip) { checkFile(program, sourceFile, rule); } break; } } } if (hasErrors) { process.exit(1); }