1# coding=utf-8 2# 3# Copyright (c) 2025 Huawei Device Co., Ltd. 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Manages source files.""" 17 18from abc import ABC, abstractmethod 19from collections.abc import Iterable 20from dataclasses import dataclass 21from io import StringIO 22from pathlib import Path 23from typing import NamedTuple 24 25from typing_extensions import override 26 27 28@dataclass(frozen=True) 29class SourceBase(ABC): 30 """Base class reprensenting all kinds of source code.""" 31 32 @property 33 @abstractmethod 34 def source_identifier(self) -> str: 35 ... 36 37 @property 38 @abstractmethod 39 def pkg_name(self) -> str: 40 ... 41 42 @abstractmethod 43 def read(self) -> str: 44 ... 45 46 47@dataclass(frozen=True) 48class SourceFile(SourceBase): 49 """Represents a file-based source code.""" 50 51 path: Path 52 53 @property 54 @override 55 def source_identifier(self) -> str: 56 return str(self.path) 57 58 @property 59 @override 60 def pkg_name(self) -> str: 61 return self.path.stem 62 63 @override 64 def read(self) -> str: 65 with open(self.path, encoding="utf-8") as f: 66 return f.read() 67 68 69@dataclass(frozen=True) 70class SourceBuffer(SourceBase): 71 """Represents a string-based source code.""" 72 73 name: str 74 buf: StringIO 75 76 @property 77 @override 78 def source_identifier(self) -> str: 79 return f"<source-buffer-{self.pkg_name}>" 80 81 @property 82 @override 83 def pkg_name(self) -> str: 84 return self.name 85 86 @override 87 def read(self) -> str: 88 return self.buf.getvalue() 89 90 91class SourceManager: 92 """Manages all input files throughout the compilation.""" 93 94 _source_collection: set[SourceBase] 95 96 def __init__(self): 97 self._source_collection = set() 98 99 @property 100 def sources(self) -> Iterable[SourceBase]: 101 return self._source_collection 102 103 def add_source(self, sb: SourceBase): 104 self._source_collection.add(sb) 105 106 107class TextPosition(NamedTuple): 108 """Represents a position within a file (1-based).""" 109 110 row: int 111 col: int 112 113 def __str__(self) -> str: 114 return f"{self.row}:{self.col}" 115 116 117class TextSpan(NamedTuple): 118 """Represents a region within a file (1-based).""" 119 120 start: TextPosition 121 stop: TextPosition 122 123 def __or__(self, other: "TextSpan") -> "TextSpan": 124 return TextSpan( 125 start=min(self.start, other.start), 126 stop=max(self.stop, other.stop), 127 ) 128 129 130@dataclass 131class SourceLocation: 132 """Represents a location (either a position or a region) within a file.""" 133 134 file: SourceBase 135 """Required: The source file associated with the location.""" 136 137 span: TextSpan | None = None 138 """Optional: The span of the location within the file.""" 139 140 def __str__(self) -> str: 141 res = self.file.source_identifier 142 if self.span: 143 res = f"{res}:{self.span.start}" 144 return res 145 146 @classmethod 147 def with_path(cls, path: Path) -> "SourceLocation": 148 """Returns a file-only source location, without any position information.""" 149 return cls(SourceFile(path))